diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml index 126d25b6ba8a..9eaafdc2da31 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 03452537adf8..b82e149cb506 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/dco.yml b/.github/dco.yml new file mode 100644 index 000000000000..4ac50ad15bbc --- /dev/null +++ b/.github/dco.yml @@ -0,0 +1,3 @@ +require: + members: false + diff --git a/.github/workflows/build-and-deploy-snapshot.yml b/.github/workflows/build-and-deploy-snapshot.yml index 75dc9e857ada..93cf0f12c168 100644 --- a/.github/workflows/build-and-deploy-snapshot.yml +++ b/.github/workflows/build-and-deploy-snapshot.yml @@ -2,7 +2,7 @@ name: Build and Deploy Snapshot on: push: branches: - - 6.2.x + - main concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: @@ -27,7 +27,7 @@ jobs: /**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false /**/framework-api-*-docs.zip::zip.type=docs /**/framework-api-*-schema.zip::zip.type=schema - build-name: 'spring-framework-6.2.x' + build-name: 'spring-framework-7.0.x' folder: 'deployment-repository' password: ${{ secrets.ARTIFACTORY_PASSWORD }} repository: 'libs-snapshot-local' diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc9a83255f68..203fd5ea6f1f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,7 @@ name: CI on: - push: - branches: - - 6.2.x + schedule: + - cron: '30 9 * * *' concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: diff --git a/.github/workflows/release-milestone.yml b/.github/workflows/release-milestone.yml new file mode 100644 index 000000000000..819a5f28695a --- /dev/null +++ b/.github/workflows/release-milestone.yml @@ -0,0 +1,96 @@ +name: Release Milestone +on: + push: + tags: + - v7.0.0-M[1-9] + - v7.0.0-RC[1-9] +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} +jobs: + build-and-stage-release: + name: Build and Stage Release + if: ${{ github.repository == 'spring-projects/spring-framework' }} + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@v4 + - name: Build and Publish + id: build-and-publish + uses: ./.github/actions/build + with: + develocity-access-key: ${{ secrets.DEVELOCITY_ACCESS_KEY }} + publish: true + - name: Stage Release + uses: spring-io/artifactory-deploy-action@26bbe925a75f4f863e1e529e85be2d0093cac116 # v0.0.1 + with: + artifact-properties: | + /**/framework-api-*.zip::zip.name=spring-framework,zip.deployed=false + /**/framework-api-*-docs.zip::zip.type=docs + /**/framework-api-*-schema.zip::zip.type=schema + build-name: ${{ format('spring-framework-{0}', steps.build-and-publish.outputs.version)}} + folder: 'deployment-repository' + password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository: 'libs-staging-local' + signing-key: ${{ secrets.GPG_PRIVATE_KEY }} + signing-passphrase: ${{ secrets.GPG_PASSPHRASE }} + uri: 'https://repo.spring.io' + username: ${{ secrets.ARTIFACTORY_USERNAME }} + outputs: + version: ${{ steps.build-and-publish.outputs.version }} + verify: + name: Verify + needs: build-and-stage-release + uses: ./.github/workflows/verify.yml + secrets: + google-chat-webhook-url: ${{ secrets.GOOGLE_CHAT_WEBHOOK_URL }} + repository-password: ${{ secrets.ARTIFACTORY_PASSWORD }} + repository-username: ${{ secrets.ARTIFACTORY_USERNAME }} + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} + with: + staging: true + version: ${{ needs.build-and-stage-release.outputs.version }} + sync-to-maven-central: + name: Sync to Maven Central + needs: + - build-and-stage-release + - verify + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Sync to Maven Central + uses: ./.github/actions/sync-to-maven-central + with: + jfrog-cli-config-token: ${{ secrets.JF_ARTIFACTORY_SPRING }} + ossrh-s01-staging-profile: ${{ secrets.OSSRH_S01_STAGING_PROFILE }} + ossrh-s01-token-password: ${{ secrets.OSSRH_S01_TOKEN_PASSWORD }} + ossrh-s01-token-username: ${{ secrets.OSSRH_S01_TOKEN_USERNAME }} + spring-framework-version: ${{ needs.build-and-stage-release.outputs.version }} + promote-release: + name: Promote Release + needs: + - build-and-stage-release + - sync-to-maven-central + runs-on: ubuntu-latest + steps: + - name: Set up JFrog CLI + uses: jfrog/setup-jfrog-cli@dff217c085c17666e8849ebdbf29c8fe5e3995e6 # v4.5.2 + env: + JF_ENV_SPRING: ${{ secrets.JF_ARTIFACTORY_SPRING }} + - name: Promote build + run: jfrog rt build-promote ${{ format('spring-framework-{0}', needs.build-and-stage-release.outputs.version)}} ${{ github.run_number }} libs-milestone-local + create-github-release: + name: Create GitHub Release + needs: + - build-and-stage-release + - promote-release + runs-on: ubuntu-latest + steps: + - name: Check Out Code + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 + - name: Create GitHub Release + uses: ./.github/actions/create-github-release + with: + milestone: ${{ needs.build-and-stage-release.outputs.version }} + pre-release: true + token: ${{ secrets.GH_ACTIONS_REPO_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 641253927c14..f6b42a55a68f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -2,7 +2,7 @@ name: Release on: push: tags: - - v6.2.[0-9]+ + - v7.0.[0-9]+ concurrency: group: ${{ github.workflow }}-${{ github.ref }} jobs: diff --git a/.gitignore b/.gitignore index 427d9621033b..9a2c0d2c9af6 100644 --- a/.gitignore +++ b/.gitignore @@ -54,3 +54,4 @@ atlassian-ide-plugin.xml cached-antora-playbook.yml node_modules +/.kotlin/ diff --git a/.sdkmanrc b/.sdkmanrc index eb2990e97224..d3514569ad76 100644 --- a/.sdkmanrc +++ b/.sdkmanrc @@ -1,3 +1,3 @@ # Enable auto-env through the sdkman_auto_env config # Add key=value pairs of SDKs to use below -java=17.0.13-librca +java=24-librca diff --git a/build.gradle b/build.gradle index 5cdeac881a92..ba18d62245c0 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,12 @@ plugins { - id 'io.freefair.aspectj' version '8.4' 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.ben-manes.versions' version '0.51.0' id 'com.github.bjornvester.xjc' version '1.8.2' apply false - id 'de.undercouch.download' version '5.4.0' id 'io.github.goooler.shadow' version '8.1.8' apply false id 'me.champeau.jmh' version '0.7.2' apply false - id 'me.champeau.mrjar' version '0.1.1' - id "net.ltgt.errorprone" version "4.1.0" apply false + id "io.spring.nullability" version "0.0.1" apply false } ext { @@ -24,13 +21,6 @@ configure(allprojects) { project -> group = "org.springframework" repositories { mavenCentral() - maven { - url = "https://repo.spring.io/milestone" - content { - // Netty 5 optional support - includeGroup 'io.projectreactor.netty' - } - } if (version.contains('-')) { maven { url = "https://repo.spring.io/milestone" } } @@ -64,7 +54,6 @@ configure([rootProject] + javaProjects) { project -> apply plugin: "java" apply plugin: "java-test-fixtures" apply plugin: 'org.springframework.build.conventions' - apply from: "${rootDir}/gradle/toolchains.gradle" apply from: "${rootDir}/gradle/ide.gradle" dependencies { @@ -80,36 +69,27 @@ configure([rootProject] + javaProjects) { project -> testRuntimeOnly("org.junit.platform:junit-platform-launcher") testRuntimeOnly("org.junit.platform:junit-platform-suite-engine") testRuntimeOnly("org.apache.logging.log4j:log4j-core") - testRuntimeOnly("org.apache.logging.log4j:log4j-jul") - testRuntimeOnly("org.apache.logging.log4j:log4j-slf4j2-impl") - // JSR-305 only used for non-required meta-annotations - compileOnly("com.google.code.findbugs:jsr305") - testCompileOnly("com.google.code.findbugs:jsr305") } ext.javadocLinks = [ "https://docs.oracle.com/en/java/javase/17/docs/api/", - "https://jakarta.ee/specifications/platform/9/apidocs/", + "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://fasterxml.github.io/jackson-core/javadoc/2.14/", - "https://fasterxml.github.io/jackson-databind/javadoc/2.14/", - "https://fasterxml.github.io/jackson-dataformat-xml/javadoc/2.14/", - "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/", - // TODO Uncomment link to JUnit 5 docs once we execute Gradle with Java 18+. - // See https://github.com/spring-projects/spring-framework/issues/27497 - // - // "https://junit.org/junit5/docs/5.12.0/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/", // Previously there could be a split-package issue between JSR250 and JSR305 javax.annotation packages, // 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://www.javadoc.io/doc/com.google.code.findbugs/jsr305/3.0.2/", + "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/README.md b/buildSrc/README.md index 9e35b5b766cf..3cf8ac690bd3 100644 --- a/buildSrc/README.md +++ b/buildSrc/README.md @@ -9,7 +9,18 @@ The `org.springframework.build.conventions` plugin applies all conventions to th * Configuring the Java compiler, see `JavaConventions` * Configuring the Kotlin compiler, see `KotlinConventions` -* Configuring testing in the build with `TestConventions` +* Configuring testing in the build with `TestConventions` +* Configuring the ArchUnit rules for the project, see `org.springframework.build.architecture.ArchitectureRules` + +This plugin also provides a DSL extension to optionally enable Java preview features for +compiling and testing sources in a module. This can be applied with the following in a +module build file: + +```groovy +springFramework { + enableJavaPreviewFeatures = true +} +``` ## Build Plugins @@ -22,6 +33,25 @@ but doesn't affect the classpath of dependent projects. This plugin does not provide a `provided` configuration, as the native `compileOnly` and `testCompileOnly` configurations are preferred. +### MultiRelease Jar + +The `org.springframework.build.multiReleaseJar` plugin configures the project with MultiRelease JAR support. +It creates a new SourceSet and dedicated tasks for each Java variant considered. +This can be configured with the DSL, by setting a list of Java variants to configure: + +```groovy +plugins { + id 'org.springframework.build.multiReleaseJar' +} + +multiRelease { + releaseVersions 21, 24 +} +``` + +Note, Java classes will be compiled with the toolchain pre-configured by the project, assuming that its +Java language version is equal or higher than all variants we consider. Each compilation task will only +set the "-release" compilation option accordingly to produce the expected bytecode version. ### RuntimeHints Java Agent diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle index 19d41d438fe4..7d0027111ba2 100644 --- a/buildSrc/build.gradle +++ b/buildSrc/build.gradle @@ -20,14 +20,23 @@ ext { dependencies { checkstyle "io.spring.javaformat:spring-javaformat-checkstyle:${javaFormatVersion}" implementation "org.jetbrains.kotlin:kotlin-gradle-plugin:${kotlinVersion}" - implementation "org.jetbrains.kotlin:kotlin-compiler-embeddable:${kotlinVersion}" - implementation "org.gradle:test-retry-gradle-plugin:1.5.6" + implementation "com.tngtech.archunit:archunit:1.4.0" + implementation "org.gradle:test-retry-gradle-plugin:1.6.2" implementation "io.spring.javaformat:spring-javaformat-gradle-plugin:${javaFormatVersion}" implementation "io.spring.nohttp:nohttp-gradle:0.0.11" + + testImplementation("org.assertj:assertj-core:${assertjVersion}") + testImplementation(platform("org.junit:junit-bom:${junitVersion}")) + testImplementation("org.junit.jupiter:junit-jupiter") + testRuntimeOnly("org.junit.platform:junit-platform-launcher") } gradlePlugin { plugins { + architecturePlugin { + id = "org.springframework.architecture" + implementationClass = "org.springframework.build.architecture.ArchitecturePlugin" + } conventionsPlugin { id = "org.springframework.build.conventions" implementationClass = "org.springframework.build.ConventionsPlugin" @@ -36,6 +45,10 @@ gradlePlugin { id = "org.springframework.build.localdev" implementationClass = "org.springframework.build.dev.LocalDevelopmentPlugin" } + multiReleasePlugin { + id = "org.springframework.build.multiReleaseJar" + implementationClass = "org.springframework.build.multirelease.MultiReleaseJarPlugin" + } optionalDependenciesPlugin { id = "org.springframework.build.optional-dependencies" implementationClass = "org.springframework.build.optional.OptionalDependenciesPlugin" @@ -46,3 +59,9 @@ gradlePlugin { } } } + +test { + useJUnitPlatform() +} + +jar.dependsOn check diff --git a/buildSrc/config/checkstyle/checkstyle.xml b/buildSrc/config/checkstyle/checkstyle.xml index c63f232e1e70..b75fed0d6583 100644 --- a/buildSrc/config/checkstyle/checkstyle.xml +++ b/buildSrc/config/checkstyle/checkstyle.xml @@ -1,6 +1,6 @@ - + @@ -12,16 +12,15 @@ - + - - - - + + + - \ No newline at end of file + diff --git a/buildSrc/gradle.properties b/buildSrc/gradle.properties index eacaf4f5b871..361684dbe054 100644 --- a/buildSrc/gradle.properties +++ b/buildSrc/gradle.properties @@ -1,2 +1,4 @@ org.gradle.caching=true -javaFormatVersion=0.0.42 +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 58f4b32dc9c9..170bb5c78e92 100644 --- a/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/CheckstyleConventions.java @@ -50,7 +50,7 @@ public void apply(Project project) { project.getPlugins().apply(CheckstylePlugin.class); project.getTasks().withType(Checkstyle.class).forEach(checkstyle -> checkstyle.getMaxHeapSize().set("1g")); CheckstyleExtension checkstyle = project.getExtensions().getByType(CheckstyleExtension.class); - checkstyle.setToolVersion("10.21.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(); @@ -63,8 +63,8 @@ private static void configureNoHttpPlugin(Project project) { project.getPlugins().apply(NoHttpPlugin.class); NoHttpExtension noHttp = project.getExtensions().getByType(NoHttpExtension.class); noHttp.setAllowlistFile(project.file("src/nohttp/allowlist.lines")); - noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", - "**/.classpath", "**/.project", "**/.gradle/**", "**/node_modules/**", "buildSrc/build/**"); + noHttp.getSource().exclude("**/test-output/**", "**/.settings/**", "**/.classpath", + "**/.project", "**/.gradle/**", "**/node_modules/**", "**/spring-jcl/**", "buildSrc/build/**"); List buildFolders = List.of("bin", "build", "out"); project.allprojects(subproject -> { Path rootPath = project.getRootDir().toPath(); diff --git a/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java b/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java index 34978ba377d2..1cd9e43cf0c4 100644 --- a/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java +++ b/buildSrc/src/main/java/org/springframework/build/ConventionsPlugin.java @@ -21,12 +21,15 @@ import org.gradle.api.plugins.JavaBasePlugin; import org.jetbrains.kotlin.gradle.plugin.KotlinBasePlugin; +import org.springframework.build.architecture.ArchitecturePlugin; + /** * Plugin to apply conventions to projects that are part of Spring Framework's build. * Conventions are applied in response to various plugins being applied. * *

When the {@link JavaBasePlugin} is applied, the conventions in {@link CheckstyleConventions}, * {@link TestConventions} and {@link JavaConventions} are applied. + * The {@link ArchitecturePlugin} plugin is also applied. * When the {@link KotlinBasePlugin} is applied, the conventions in {@link KotlinConventions} * are applied. * @@ -36,6 +39,8 @@ public class ConventionsPlugin implements Plugin { @Override public void apply(Project project) { + project.getExtensions().create("springFramework", SpringFrameworkExtension.class); + new ArchitecturePlugin().apply(project); new CheckstyleConventions().apply(project); new JavaConventions().apply(project); new KotlinConventions().apply(project); diff --git a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java index 60b791799f52..634f1ce5924e 100644 --- a/buildSrc/src/main/java/org/springframework/build/JavaConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/JavaConventions.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,7 +17,6 @@ package org.springframework.build; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import org.gradle.api.Plugin; @@ -42,8 +41,21 @@ public class JavaConventions { private static final List TEST_COMPILER_ARGS; + /** + * 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). + */ + private static final JavaLanguageVersion DEFAULT_RELEASE_VERSION = JavaLanguageVersion.of(17); + static { - List commonCompilerArgs = Arrays.asList( + List commonCompilerArgs = List.of( "-Xlint:serial", "-Xlint:cast", "-Xlint:classfile", "-Xlint:dep-ann", "-Xlint:divzero", "-Xlint:empty", "-Xlint:finally", "-Xlint:overrides", "-Xlint:path", "-Xlint:processing", "-Xlint:static", "-Xlint:try", "-Xlint:-options", @@ -51,43 +63,75 @@ public class JavaConventions { ); COMPILER_ARGS = new ArrayList<>(); COMPILER_ARGS.addAll(commonCompilerArgs); - COMPILER_ARGS.addAll(Arrays.asList( + COMPILER_ARGS.addAll(List.of( "-Xlint:varargs", "-Xlint:fallthrough", "-Xlint:rawtypes", "-Xlint:deprecation", "-Xlint:unchecked", "-Werror" )); TEST_COMPILER_ARGS = new ArrayList<>(); TEST_COMPILER_ARGS.addAll(commonCompilerArgs); - TEST_COMPILER_ARGS.addAll(Arrays.asList("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes", + TEST_COMPILER_ARGS.addAll(List.of("-Xlint:-varargs", "-Xlint:-fallthrough", "-Xlint:-rawtypes", "-Xlint:-deprecation", "-Xlint:-unchecked")); } public void apply(Project project) { - project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> applyJavaCompileConventions(project)); + project.getPlugins().withType(JavaBasePlugin.class, javaPlugin -> { + applyToolchainConventions(project); + applyJavaCompileConventions(project); + }); } /** - * Applies the common Java compiler options for main sources, test fixture sources, and - * test sources. + * Configure the Toolchain support for the project. * @param project the current project */ - private void applyJavaCompileConventions(Project project) { + private static void applyToolchainConventions(Project project) { project.getExtensions().getByType(JavaPluginExtension.class).toolchain(toolchain -> { toolchain.getVendor().set(JvmVendorSpec.BELLSOFT); - toolchain.getLanguageVersion().set(JavaLanguageVersion.of(17)); + toolchain.getLanguageVersion().set(DEFAULT_LANGUAGE_VERSION); + }); + } + + /** + * Apply the common Java compiler options for main sources, test fixture sources, and + * test sources. + * @param project the current project + */ + private void applyJavaCompileConventions(Project project) { + project.afterEvaluate(p -> { + p.getTasks().withType(JavaCompile.class) + .matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_JAVA_TASK_NAME)) + .forEach(compileTask -> { + compileTask.getOptions().setCompilerArgs(COMPILER_ARGS); + compileTask.getOptions().setEncoding("UTF-8"); + setJavaRelease(compileTask); + }); + p.getTasks().withType(JavaCompile.class) + .matching(compileTask -> compileTask.getName().startsWith(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME) + || compileTask.getName().equals("compileTestFixturesJava")) + .forEach(compileTask -> { + compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS); + compileTask.getOptions().setEncoding("UTF-8"); + setJavaRelease(compileTask); + }); + }); - project.getTasks().withType(JavaCompile.class) - .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_JAVA_TASK_NAME)) - .forEach(compileTask -> { - compileTask.getOptions().setCompilerArgs(COMPILER_ARGS); - compileTask.getOptions().setEncoding("UTF-8"); - }); - project.getTasks().withType(JavaCompile.class) - .matching(compileTask -> compileTask.getName().equals(JavaPlugin.COMPILE_TEST_JAVA_TASK_NAME) - || compileTask.getName().equals("compileTestFixturesJava")) - .forEach(compileTask -> { - compileTask.getOptions().setCompilerArgs(TEST_COMPILER_ARGS); - compileTask.getOptions().setEncoding("UTF-8"); - }); + } + + /** + * We should pick the {@link #DEFAULT_RELEASE_VERSION} for all compiled classes, + * unless the current task is compiling multi-release JAR code with a higher version. + */ + private void setJavaRelease(JavaCompile task) { + int defaultVersion = DEFAULT_RELEASE_VERSION.asInt(); + int releaseVersion = defaultVersion; + int compilerVersion = task.getJavaCompiler().get().getMetadata().getLanguageVersion().asInt(); + for (int version = defaultVersion ; version <= compilerVersion ; version++) { + if (task.getName().contains("Java" + version)) { + releaseVersion = version; + break; + } + } + task.getOptions().getRelease().set(releaseVersion); } } diff --git a/buildSrc/src/main/java/org/springframework/build/KotlinConventions.java b/buildSrc/src/main/java/org/springframework/build/KotlinConventions.java index 438501d228f4..388c324ffc6d 100644 --- a/buildSrc/src/main/java/org/springframework/build/KotlinConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/KotlinConventions.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. @@ -16,15 +16,14 @@ package org.springframework.build; -import java.util.ArrayList; -import java.util.List; - import org.gradle.api.Project; -import org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions; +import org.jetbrains.kotlin.gradle.dsl.JvmTarget; +import org.jetbrains.kotlin.gradle.dsl.KotlinVersion; import org.jetbrains.kotlin.gradle.tasks.KotlinCompile; /** * @author Brian Clozel + * @author Sebastien Deleuze */ public class KotlinConventions { @@ -34,15 +33,19 @@ void apply(Project project) { } private void configure(KotlinCompile compile) { - KotlinJvmOptions kotlinOptions = compile.getKotlinOptions(); - kotlinOptions.setApiVersion("1.7"); - kotlinOptions.setLanguageVersion("1.7"); - kotlinOptions.setJvmTarget("17"); - kotlinOptions.setJavaParameters(true); - kotlinOptions.setAllWarningsAsErrors(true); - List freeCompilerArgs = new ArrayList<>(compile.getKotlinOptions().getFreeCompilerArgs()); - freeCompilerArgs.addAll(List.of("-Xsuppress-version-warnings", "-Xjsr305=strict", "-opt-in=kotlin.RequiresOptIn")); - compile.getKotlinOptions().setFreeCompilerArgs(freeCompilerArgs); + compile.compilerOptions(options -> { + options.getApiVersion().set(KotlinVersion.KOTLIN_2_1); + options.getLanguageVersion().set(KotlinVersion.KOTLIN_2_1); + options.getJvmTarget().set(JvmTarget.JVM_17); + options.getJavaParameters().set(true); + options.getAllWarningsAsErrors().set(true); + options.getFreeCompilerArgs().addAll( + "-Xsuppress-version-warnings", + "-Xjsr305=strict", // For dependencies using JSR 305 + "-opt-in=kotlin.RequiresOptIn", + "-Xjdk-release=17" // Needed due to https://youtrack.jetbrains.com/issue/KT-49746 + ); + }); } } diff --git a/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java b/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.java new file mode 100644 index 000000000000..c4001388eb05 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/SpringFrameworkExtension.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.build; + +import java.util.Collections; +import java.util.List; + +import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.api.tasks.testing.Test; +import org.gradle.process.CommandLineArgumentProvider; + +public class SpringFrameworkExtension { + + private final Property enableJavaPreviewFeatures; + + public SpringFrameworkExtension(Project project) { + this.enableJavaPreviewFeatures = project.getObjects().property(Boolean.class); + project.getTasks().withType(JavaCompile.class).configureEach(javaCompile -> + javaCompile.getOptions().getCompilerArgumentProviders().add(asArgumentProvider())); + project.getTasks().withType(Test.class).configureEach(test -> + test.getJvmArgumentProviders().add(asArgumentProvider())); + + } + + public Property getEnableJavaPreviewFeatures() { + return this.enableJavaPreviewFeatures; + } + + private CommandLineArgumentProvider asArgumentProvider() { + return () -> { + if (getEnableJavaPreviewFeatures().getOrElse(false)) { + return List.of("--enable-preview"); + } + return Collections.emptyList(); + }; + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/TestConventions.java b/buildSrc/src/main/java/org/springframework/build/TestConventions.java index 1283d233765d..e6e2dd0d9a9b 100644 --- a/buildSrc/src/main/java/org/springframework/build/TestConventions.java +++ b/buildSrc/src/main/java/org/springframework/build/TestConventions.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,8 @@ 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 @@ * * @author Brian Clozel * @author Andy Wilkinson + * @author Sam Brannen */ class TestConventions { @@ -50,14 +53,16 @@ private void configureTestConventions(Project project) { } 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", - "io.netty.leakDetection.level", "paranoid", - "io.netty5.leakDetectionLevel", "paranoid", - "io.netty5.leakDetection.targetRecords", "32", - "io.netty5.buffer.lifecycleTracingEnabled", "true" + "io.netty.leakDetection.level", "paranoid" )); if (project.hasProperty("testGroups")) { test.systemProperty("testGroups", project.getProperties().get("testGroups")); diff --git a/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureCheck.java b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureCheck.java new file mode 100644 index 000000000000..ef150a5321d7 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureCheck.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2025 the original author 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.build.architecture; + +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.EvaluationResult; +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.StandardOpenOption; +import java.util.List; +import org.gradle.api.DefaultTask; +import org.gradle.api.GradleException; +import org.gradle.api.Task; +import org.gradle.api.file.DirectoryProperty; +import org.gradle.api.file.FileCollection; +import org.gradle.api.file.FileTree; +import org.gradle.api.provider.ListProperty; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.IgnoreEmptyDirectories; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.InputFiles; +import org.gradle.api.tasks.Internal; +import org.gradle.api.tasks.Optional; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.tasks.PathSensitive; +import org.gradle.api.tasks.PathSensitivity; +import org.gradle.api.tasks.SkipWhenEmpty; +import org.gradle.api.tasks.TaskAction; + +import static org.springframework.build.architecture.ArchitectureRules.allPackagesShouldBeFreeOfTangles; +import static org.springframework.build.architecture.ArchitectureRules.classesShouldNotImportForbiddenTypes; +import static org.springframework.build.architecture.ArchitectureRules.javaClassesShouldNotImportKotlinAnnotations; +import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToLowerCaseWithoutLocale; +import static org.springframework.build.architecture.ArchitectureRules.noClassesShouldCallStringToUpperCaseWithoutLocale; +import static org.springframework.build.architecture.ArchitectureRules.packageInfoShouldBeNullMarked; + +/** + * {@link Task} that checks for architecture problems. + * + * @author Andy Wilkinson + * @author Scott Frederick + */ +public abstract class ArchitectureCheck extends DefaultTask { + + private FileCollection classes; + + public ArchitectureCheck() { + getOutputDirectory().convention(getProject().getLayout().getBuildDirectory().dir(getName())); + getProhibitObjectsRequireNonNull().convention(true); + getRules().addAll(packageInfoShouldBeNullMarked(), + classesShouldNotImportForbiddenTypes(), + javaClassesShouldNotImportKotlinAnnotations(), + allPackagesShouldBeFreeOfTangles(), + noClassesShouldCallStringToLowerCaseWithoutLocale(), + noClassesShouldCallStringToUpperCaseWithoutLocale()); + getRuleDescriptions().set(getRules().map((rules) -> rules.stream().map(ArchRule::getDescription).toList())); + } + + @TaskAction + void checkArchitecture() throws IOException { + JavaClasses javaClasses = new ClassFileImporter() + .importPaths(this.classes.getFiles().stream().map(File::toPath).toList()); + List violations = getRules().get() + .stream() + .map((rule) -> rule.evaluate(javaClasses)) + .filter(EvaluationResult::hasViolation) + .toList(); + File outputFile = getOutputDirectory().file("failure-report.txt").get().getAsFile(); + outputFile.getParentFile().mkdirs(); + if (!violations.isEmpty()) { + StringBuilder report = new StringBuilder(); + for (EvaluationResult violation : violations) { + report.append(violation.getFailureReport()); + report.append(String.format("%n")); + } + Files.writeString(outputFile.toPath(), report.toString(), StandardOpenOption.CREATE, + StandardOpenOption.TRUNCATE_EXISTING); + throw new GradleException("Architecture check failed. See '" + outputFile + "' for details."); + } + else { + outputFile.createNewFile(); + } + } + + public void setClasses(FileCollection classes) { + this.classes = classes; + } + + @Internal + public FileCollection getClasses() { + return this.classes; + } + + @InputFiles + @SkipWhenEmpty + @IgnoreEmptyDirectories + @PathSensitive(PathSensitivity.RELATIVE) + final FileTree getInputClasses() { + return this.classes.getAsFileTree(); + } + + @Optional + @InputFiles + @PathSensitive(PathSensitivity.RELATIVE) + public abstract DirectoryProperty getResourcesDirectory(); + + @OutputDirectory + public abstract DirectoryProperty getOutputDirectory(); + + @Internal + public abstract ListProperty getRules(); + + @Internal + public abstract Property getProhibitObjectsRequireNonNull(); + + @Input + // The rules themselves can't be an input as they aren't serializable so we use + // their descriptions instead + abstract ListProperty getRuleDescriptions(); +} diff --git a/buildSrc/src/main/java/org/springframework/build/architecture/ArchitecturePlugin.java b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitecturePlugin.java new file mode 100644 index 000000000000..22fdcef2b2de --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitecturePlugin.java @@ -0,0 +1,74 @@ +/* + * Copyright 2002-2025 the original author 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.build.architecture; + +import java.util.ArrayList; +import java.util.List; +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.Task; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.language.base.plugins.LifecycleBasePlugin; + +/** + * {@link Plugin} for verifying a project's architecture. + * + * @author Andy Wilkinson + */ +public class ArchitecturePlugin implements Plugin { + + @Override + public void apply(Project project) { + project.getPlugins().withType(JavaPlugin.class, (javaPlugin) -> registerTasks(project)); + } + + private void registerTasks(Project project) { + JavaPluginExtension javaPluginExtension = project.getExtensions().getByType(JavaPluginExtension.class); + List> architectureChecks = new ArrayList<>(); + for (SourceSet sourceSet : javaPluginExtension.getSourceSets()) { + if (sourceSet.getName().contains("test")) { + // skip test source sets. + continue; + } + TaskProvider checkArchitecture = project.getTasks() + .register(taskName(sourceSet), ArchitectureCheck.class, + (task) -> { + task.setClasses(sourceSet.getOutput().getClassesDirs()); + task.getResourcesDirectory().set(sourceSet.getOutput().getResourcesDir()); + task.dependsOn(sourceSet.getProcessResourcesTaskName()); + task.setDescription("Checks the architecture of the classes of the " + sourceSet.getName() + + " source set."); + task.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); + }); + architectureChecks.add(checkArchitecture); + } + if (!architectureChecks.isEmpty()) { + TaskProvider checkTask = project.getTasks().named(LifecycleBasePlugin.CHECK_TASK_NAME); + checkTask.configure((check) -> check.dependsOn(architectureChecks)); + } + } + + private static String taskName(SourceSet sourceSet) { + return "checkArchitecture" + + sourceSet.getName().substring(0, 1).toUpperCase() + + sourceSet.getName().substring(1); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureRules.java b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureRules.java new file mode 100644 index 000000000000..56a04071b96a --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/architecture/ArchitectureRules.java @@ -0,0 +1,107 @@ +/* + * Copyright 2002-2025 the original author 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.build.architecture; + +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import com.tngtech.archunit.library.dependencies.SliceAssignment; +import com.tngtech.archunit.library.dependencies.SliceIdentifier; +import com.tngtech.archunit.library.dependencies.SlicesRuleDefinition; +import java.util.List; + +abstract class ArchitectureRules { + + static ArchRule allPackagesShouldBeFreeOfTangles() { + return SlicesRuleDefinition.slices() + .assignedFrom(new SpringSlices()).should().beFreeOfCycles(); + } + + static ArchRule noClassesShouldCallStringToLowerCaseWithoutLocale() { + return ArchRuleDefinition.noClasses() + .should() + .callMethod(String.class, "toLowerCase") + .because("String.toLowerCase(Locale.ROOT) should be used instead"); + } + + static ArchRule noClassesShouldCallStringToUpperCaseWithoutLocale() { + return ArchRuleDefinition.noClasses() + .should() + .callMethod(String.class, "toUpperCase") + .because("String.toUpperCase(Locale.ROOT) should be used instead"); + } + + static ArchRule packageInfoShouldBeNullMarked() { + return ArchRuleDefinition.classes() + .that().haveSimpleName("package-info") + .should().beAnnotatedWith("org.jspecify.annotations.NullMarked") + .allowEmptyShould(true); + } + + static ArchRule classesShouldNotImportForbiddenTypes() { + return ArchRuleDefinition.noClasses() + .should().dependOnClassesThat() + .haveFullyQualifiedName("reactor.core.support.Assert") + .orShould().dependOnClassesThat() + .haveFullyQualifiedName("org.slf4j.LoggerFactory") + .orShould().dependOnClassesThat() + .haveFullyQualifiedName("org.springframework.lang.NonNull") + .orShould().dependOnClassesThat() + .haveFullyQualifiedName("org.springframework.lang.Nullable"); + } + + static ArchRule javaClassesShouldNotImportKotlinAnnotations() { + return ArchRuleDefinition.noClasses() + .that(new DescribedPredicate("is not a Kotlin class") { + @Override + public boolean test(JavaClass javaClass) { + return javaClass.getSourceCodeLocation() + .getSourceFileName().endsWith(".java"); + } + } + ) + .should().dependOnClassesThat() + .resideInAnyPackage("org.jetbrains.annotations..") + .allowEmptyShould(true); + } + + static class SpringSlices implements SliceAssignment { + + private final List ignoredPackages = List.of("org.springframework.asm", + "org.springframework.cglib", + "org.springframework.javapoet", + "org.springframework.objenesis"); + + @Override + public SliceIdentifier getIdentifierOf(JavaClass javaClass) { + + String packageName = javaClass.getPackageName(); + for (String ignoredPackage : ignoredPackages) { + if (packageName.startsWith(ignoredPackage)) { + return SliceIdentifier.ignore(); + } + } + return SliceIdentifier.of("spring framework"); + } + + @Override + public String getDescription() { + return "Spring Framework Slices"; + } + } +} diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java new file mode 100644 index 000000000000..547c3f480de4 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseExtension.java @@ -0,0 +1,139 @@ +/* + * Copyright 2002-2025 the original author 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.build.multirelease; + +import javax.inject.Inject; + +import org.gradle.api.artifacts.Configuration; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.attributes.LibraryElements; +import org.gradle.api.file.ConfigurableFileCollection; +import org.gradle.api.file.FileCollection; +import org.gradle.api.java.archives.Attributes; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.tasks.SourceSet; +import org.gradle.api.tasks.SourceSetContainer; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.api.tasks.TaskProvider; +import org.gradle.api.tasks.bundling.Jar; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.api.tasks.testing.Test; +import org.gradle.language.base.plugins.LifecycleBasePlugin; + +/** + * @author Cedric Champeau + * @author Brian Clozel + */ +public abstract class MultiReleaseExtension { + private final TaskContainer tasks; + private final SourceSetContainer sourceSets; + private final DependencyHandler dependencies; + private final ObjectFactory objects; + private final ConfigurationContainer configurations; + + @Inject + public MultiReleaseExtension(SourceSetContainer sourceSets, + ConfigurationContainer configurations, + TaskContainer tasks, + DependencyHandler dependencies, + ObjectFactory objectFactory) { + this.sourceSets = sourceSets; + this.configurations = configurations; + this.tasks = tasks; + this.dependencies = dependencies; + this.objects = objectFactory; + } + + public void releaseVersions(int... javaVersions) { + releaseVersions("src/main/", "src/test/", javaVersions); + } + + private void releaseVersions(String mainSourceDirectory, String testSourceDirectory, int... javaVersions) { + for (int javaVersion : javaVersions) { + addLanguageVersion(javaVersion, mainSourceDirectory, testSourceDirectory); + } + } + + private void addLanguageVersion(int javaVersion, String mainSourceDirectory, String testSourceDirectory) { + String javaN = "java" + javaVersion; + + SourceSet langSourceSet = sourceSets.create(javaN, srcSet -> srcSet.getJava().srcDir(mainSourceDirectory + javaN)); + SourceSet testSourceSet = sourceSets.create(javaN + "Test", srcSet -> srcSet.getJava().srcDir(testSourceDirectory + javaN)); + SourceSet sharedSourceSet = sourceSets.findByName(SourceSet.MAIN_SOURCE_SET_NAME); + SourceSet sharedTestSourceSet = sourceSets.findByName(SourceSet.TEST_SOURCE_SET_NAME); + + FileCollection mainClasses = objects.fileCollection().from(sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).getOutput().getClassesDirs()); + dependencies.add(javaN + "Implementation", mainClasses); + + tasks.named(langSourceSet.getCompileJavaTaskName(), JavaCompile.class, task -> + task.getOptions().getRelease().set(javaVersion) + ); + tasks.named(testSourceSet.getCompileJavaTaskName(), JavaCompile.class, task -> + task.getOptions().getRelease().set(javaVersion) + ); + + TaskProvider testTask = createTestTask(javaVersion, testSourceSet, sharedTestSourceSet, langSourceSet, sharedSourceSet); + tasks.named("check", task -> task.dependsOn(testTask)); + + configureMultiReleaseJar(javaVersion, langSourceSet); + } + + private TaskProvider createTestTask(int javaVersion, SourceSet testSourceSet, SourceSet sharedTestSourceSet, SourceSet langSourceSet, SourceSet sharedSourceSet) { + Configuration testImplementation = configurations.getByName(testSourceSet.getImplementationConfigurationName()); + testImplementation.extendsFrom(configurations.getByName(sharedTestSourceSet.getImplementationConfigurationName())); + Configuration testCompileOnly = configurations.getByName(testSourceSet.getCompileOnlyConfigurationName()); + testCompileOnly.extendsFrom(configurations.getByName(sharedTestSourceSet.getCompileOnlyConfigurationName())); + testCompileOnly.getDependencies().add(dependencies.create(langSourceSet.getOutput().getClassesDirs())); + testCompileOnly.getDependencies().add(dependencies.create(sharedSourceSet.getOutput().getClassesDirs())); + + Configuration testRuntimeClasspath = configurations.getByName(testSourceSet.getRuntimeClasspathConfigurationName()); + // so here's the deal. MRjars are JARs! Which means that to execute tests, we need + // the JAR on classpath, not just classes + resources as Gradle usually does + testRuntimeClasspath.getAttributes() + .attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objects.named(LibraryElements.class, LibraryElements.JAR)); + + TaskProvider testTask = tasks.register("java" + javaVersion + "Test", Test.class, test -> { + test.setGroup(LifecycleBasePlugin.VERIFICATION_GROUP); + + ConfigurableFileCollection testClassesDirs = objects.fileCollection(); + testClassesDirs.from(testSourceSet.getOutput()); + testClassesDirs.from(sharedTestSourceSet.getOutput()); + test.setTestClassesDirs(testClassesDirs); + ConfigurableFileCollection classpath = objects.fileCollection(); + // must put the MRJar first on classpath + classpath.from(tasks.named("jar")); + // then we put the specific test sourceset tests, so that we can override + // the shared versions + classpath.from(testSourceSet.getOutput()); + + // then we add the shared tests + classpath.from(sharedTestSourceSet.getRuntimeClasspath()); + test.setClasspath(classpath); + }); + return testTask; + } + + private void configureMultiReleaseJar(int version, SourceSet languageSourceSet) { + tasks.named("jar", Jar.class, jar -> { + jar.into("META-INF/versions/" + version, s -> s.from(languageSourceSet.getOutput())); + Attributes attributes = jar.getManifest().getAttributes(); + attributes.put("Multi-Release", "true"); + }); + } + +} diff --git a/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java new file mode 100644 index 000000000000..1716d016b285 --- /dev/null +++ b/buildSrc/src/main/java/org/springframework/build/multirelease/MultiReleaseJarPlugin.java @@ -0,0 +1,61 @@ +/* + * Copyright 2002-2025 the original author 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.build.multirelease; + +import javax.inject.Inject; + +import org.gradle.api.Plugin; +import org.gradle.api.Project; +import org.gradle.api.artifacts.ConfigurationContainer; +import org.gradle.api.artifacts.dsl.DependencyHandler; +import org.gradle.api.model.ObjectFactory; +import org.gradle.api.plugins.ExtensionContainer; +import org.gradle.api.plugins.JavaPlugin; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.tasks.TaskContainer; +import org.gradle.jvm.toolchain.JavaToolchainService; + +/** + * A plugin which adds support for building multi-release jars + * with Gradle. + * @author Cedric Champeau + * @author Brian Clozel + * @see original project + */ +public class MultiReleaseJarPlugin implements Plugin { + + @Inject + protected JavaToolchainService getToolchains() { + throw new UnsupportedOperationException(); + } + + public void apply(Project project) { + project.getPlugins().apply(JavaPlugin.class); + ExtensionContainer extensions = project.getExtensions(); + JavaPluginExtension javaPluginExtension = extensions.getByType(JavaPluginExtension.class); + ConfigurationContainer configurations = project.getConfigurations(); + TaskContainer tasks = project.getTasks(); + DependencyHandler dependencies = project.getDependencies(); + ObjectFactory objects = project.getObjects(); + extensions.create("multiRelease", MultiReleaseExtension.class, + javaPluginExtension.getSourceSets(), + configurations, + tasks, + dependencies, + objects); + } +} diff --git a/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java new file mode 100644 index 000000000000..b4d2f618803f --- /dev/null +++ b/buildSrc/src/test/java/org/springframework/build/multirelease/MultiReleaseJarPluginTests.java @@ -0,0 +1,137 @@ +/* + * Copyright 2002-2025 the original author 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.build.multirelease; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.io.PrintWriter; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.jar.Attributes; +import java.util.jar.JarFile; +import org.gradle.testkit.runner.BuildResult; +import org.gradle.testkit.runner.GradleRunner; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link MultiReleaseJarPlugin} + */ +public class MultiReleaseJarPluginTests { + + private File projectDir; + + private File buildFile; + + @BeforeEach + void setup(@TempDir File projectDir) { + this.projectDir = projectDir; + this.buildFile = new File(this.projectDir, "build.gradle"); + } + + @Test + void configureSourceSets() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + multiRelease { releaseVersions 21, 24 } + task printSourceSets { + doLast { + sourceSets.all { println it.name } + } + } + """); + BuildResult buildResult = runGradle("printSourceSets"); + assertThat(buildResult.getOutput()).contains("main", "test", "java21", "java21Test", "java24", "java24Test"); + } + + @Test + void configureToolchainReleaseVersion() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + multiRelease { releaseVersions 21 } + task printReleaseVersion { + doLast { + tasks.all { println it.name } + tasks.named("compileJava21Java") { + println "compileJava21Java releaseVersion: ${it.options.release.get()}" + } + tasks.named("compileJava21TestJava") { + println "compileJava21TestJava releaseVersion: ${it.options.release.get()}" + } + } + } + """); + + BuildResult buildResult = runGradle("printReleaseVersion"); + assertThat(buildResult.getOutput()).contains("compileJava21Java releaseVersion: 21") + .contains("compileJava21TestJava releaseVersion: 21"); + } + + @Test + void packageInJar() throws IOException { + writeBuildFile(""" + plugins { + id 'java' + id 'org.springframework.build.multiReleaseJar' + } + version = '1.2.3' + multiRelease { releaseVersions 17 } + """); + writeClass("src/main/java17", "Main.java", """ + public class Main {} + """); + BuildResult buildResult = runGradle("assemble"); + File file = new File(this.projectDir, "/build/libs/" + this.projectDir.getName() + "-1.2.3.jar"); + assertThat(file).exists(); + try (JarFile jar = new JarFile(file)) { + Attributes mainAttributes = jar.getManifest().getMainAttributes(); + assertThat(mainAttributes.getValue("Multi-Release")).isEqualTo("true"); + + assertThat(jar.entries().asIterator()).toIterable() + .anyMatch(entry -> entry.getName().equals("META-INF/versions/17/Main.class")); + } + } + + private void writeBuildFile(String buildContent) throws IOException { + try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) { + out.print(buildContent); + } + } + + private void writeClass(String path, String fileName, String fileContent) throws IOException { + Path folder = this.projectDir.toPath().resolve(path); + Files.createDirectories(folder); + Path filePath = folder.resolve(fileName); + Files.createFile(filePath); + Files.writeString(filePath, fileContent); + } + + private BuildResult runGradle(String... args) { + return GradleRunner.create().withProjectDir(this.projectDir).withArguments(args).withPluginClasspath().build(); + } + +} diff --git a/framework-api/framework-api.gradle b/framework-api/framework-api.gradle index c8456268c14c..f9da96bcd091 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 5850c2f51d41..84527746bd80 100644 --- a/framework-docs/framework-docs.gradle +++ b/framework-docs/framework-docs.gradle @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask + plugins { id 'kotlin' id 'io.spring.antora.generate-antora-yml' version '0.0.1' @@ -41,34 +43,41 @@ repositories { } } -dependencies { - api(project(":spring-aspects")) - api(project(":spring-context")) - api(project(":spring-context-support")) - 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("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.eclipse.jetty.websocket:jetty-websocket-jetty-api") - api("org.jetbrains.kotlin:kotlin-stdlib") +// To avoid a redeclaration error with Kotlin compiler +tasks.named('compileKotlin', KotlinCompilationTask.class) { + javaSources.from = [] +} +dependencies { + 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 da2650dcec0d..f8389c3fdcfa 100644 --- a/framework-docs/modules/ROOT/nav.adoc +++ b/framework-docs/modules/ROOT/nav.adoc @@ -32,6 +32,7 @@ **** xref:core/beans/java/bean-annotation.adoc[] **** xref:core/beans/java/configuration-annotation.adoc[] **** xref:core/beans/java/composing-configuration-classes.adoc[] +**** xref:core/beans/java/programmatic-bean-registration.adoc[] *** xref:core/beans/environment.adoc[] *** xref:core/beans/context-load-time-weaver.adoc[] *** xref:core/beans/context-introduction.adoc[] @@ -101,7 +102,6 @@ *** xref:core/aop-api/extensibility.adoc[] ** xref:core/null-safety.adoc[] ** xref:core/databuffer-codec.adoc[] -** xref:core/spring-jcl.adoc[] ** xref:core/aot.adoc[] ** xref:core/appendix.adoc[] *** xref:core/appendix/xsd-schemas.adoc[] @@ -161,7 +161,6 @@ **** xref:web/webmvc/mvc-servlet/exceptionhandlers.adoc[] **** xref:web/webmvc/mvc-servlet/viewresolver.adoc[] **** xref:web/webmvc/mvc-servlet/localeresolver.adoc[] -**** xref:web/webmvc/mvc-servlet/themeresolver.adoc[] **** xref:web/webmvc/mvc-servlet/multipart.adoc[] **** xref:web/webmvc/mvc-servlet/logging.adoc[] *** xref:web/webmvc/filters.adoc[] @@ -198,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[] @@ -226,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[] @@ -293,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[] @@ -433,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 896453d8e912..6e7e5cecd0e0 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 80a75965d774..ce75e7fa594d 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 13f20afe733d..d91aaafb19b4 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 8c4697771ab9..7bc227cdc0d2 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") ---- ====== @@ -206,18 +206,17 @@ another file or files. The following example shows how to do so: - ---- -In the preceding example, external bean definitions are loaded from three files: -`services.xml`, `messageSource.xml`, and `themeSource.xml`. All location paths are +In the preceding example, external bean definitions are loaded from the files +`services.xml` and `messageSource.xml`. All location paths are relative to the definition file doing the importing, so `services.xml` must be in the same directory or classpath location as the file doing the importing, while -`messageSource.xml` and `themeSource.xml` must be in a `resources` location below the +`messageSource.xml` must be in a `resources` location below the location of the importing file. As you can see, a leading slash is ignored. However, given that these paths are relative, it is better form not to use the slash at all. The contents of the files being imported, including the top level `` element, must @@ -310,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/classpath-scanning.adoc b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc index 8193305116be..611009b73f49 100644 --- a/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc +++ b/framework-docs/modules/ROOT/pages/core/beans/classpath-scanning.adoc @@ -674,9 +674,7 @@ By default, the `AnnotationBeanNameGenerator` is used. For Spring xref:core/beans/classpath-scanning.adoc#beans-stereotype-annotations[stereotype annotations], if you supply a name via the annotation's `value` attribute that name will be used as the name in the corresponding bean definition. This convention also applies when the -following JSR-250 and JSR-330 annotations are used instead of Spring stereotype -annotations: `@jakarta.annotation.ManagedBean`, `@javax.annotation.ManagedBean`, -`@jakarta.inject.Named`, and `@javax.inject.Named`. +`@jakarta.inject.Named` annotation is used instead of Spring stereotype annotations. As of Spring Framework 6.1, the name of the annotation attribute that is used to specify the bean name is no longer required to be `value`. Custom stereotype annotations can 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 612185813e2f..537b90cc5f8f 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 cf9e68e3a8eb..56641fd847eb 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. - -With the `context` namespace introduced in Spring 2.5, you can configure property placeholders +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, 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/beans/java/programmatic-bean-registration.adoc b/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc new file mode 100644 index 000000000000..f50c94025568 --- /dev/null +++ b/framework-docs/modules/ROOT/pages/core/beans/java/programmatic-bean-registration.adoc @@ -0,0 +1,88 @@ +[[beans-java-programmatic-registration]] += Programmatic Bean Registration + +As of Spring Framework 7, a first-class support for programmatic bean registration is +provided via the {spring-framework-api}/beans/factory/BeanRegistrar.html[`BeanRegistrar`] +interface that can be implemented to register beans programmatically in a flexible and +efficient way. + +Those bean registrar implementations are typically imported with an `@Import` annotation +on `@Configuration` classes. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @Import(MyBeanRegistrar.class) + class MyConfiguration { + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + @Configuration + @Import(MyBeanRegistrar::class) + class MyConfiguration { + } +---- +====== + +NOTE: You can leverage type-level conditional annotations ({spring-framework-api}/context/annotation/Conditional.html[`@Conditional`], +but also other variants) to conditionally import the related bean registrars. + +The bean registrar implementation uses {spring-framework-api}/beans/factory/BeanRegistry.html[`BeanRegistry`] and +{spring-framework-api}/core/env/Environment.html[`Environment`] APIs to register beans programmatically in a concise +and flexible way. For example, it allows custom registration through an `if` expression, a +`for` loop, etc. + +[tabs] +====== +Java:: ++ +[source,java,indent=0,subs="verbatim,quotes"] +---- + class MyBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class); + registry.registerBean("bar", Bar.class, spec -> spec + .prototype() + .lazyInit() + .description("Custom description") + .supplier(context -> new Bar(context.bean(Foo.class)))); + if (env.matchesProfiles("baz")) { + registry.registerBean(Baz.class, spec -> spec + .supplier(context -> new Baz("Hello World!"))); + } + } + } +---- + +Kotlin:: ++ +[source,kotlin,indent=0,subs="verbatim,quotes"] +---- + class MyBeanRegistrar : BeanRegistrarDsl({ + registerBean() + registerBean( + name = "bar", + prototype = true, + lazyInit = true, + description = "Custom description") { + Bar(bean()) + } + profile("baz") { + registerBean { Baz("Hello World!") } + } + }) +---- +====== + +NOTE: Bean registrars are supported with xref:core/aot.adoc[Ahead of Time Optimizations], +either on the JVM or with GraalVM native images, including when instance suppliers are used. diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-elvis.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-elvis.adoc index 5880f13f5d97..e4c2ff636d3f 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-elvis.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-elvis.adoc @@ -1,19 +1,19 @@ [[expressions-operator-elvis]] = The Elvis Operator -The Elvis operator is a shortening of the ternary operator syntax and is used in the -https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language. -With the ternary operator syntax, you usually have to repeat a variable twice, as the -following example shows: +The Elvis operator (`?:`) is a shortening of the ternary operator syntax and is used in +the https://www.groovy-lang.org/operators.html#_elvis_operator[Groovy] language. With the +ternary operator syntax, you often have to repeat a variable twice, as the following Java +example shows: -[source,groovy,indent=0,subs="verbatim,quotes"] +[source,java,indent=0,subs="verbatim,quotes"] ---- String name = "Elvis Presley"; String displayName = (name != null ? name : "Unknown"); ---- Instead, you can use the Elvis operator (named for the resemblance to Elvis' hair style). -The following example shows how to use the Elvis operator: +The following example shows how to use the Elvis operator in a SpEL expression: [tabs] ====== @@ -23,7 +23,7 @@ Java:: ---- ExpressionParser parser = new SpelExpressionParser(); - String name = parser.parseExpression("name?:'Unknown'").getValue(new Inventor(), String.class); + String name = parser.parseExpression("name ?: 'Unknown'").getValue(new Inventor(), String.class); System.out.println(name); // 'Unknown' ---- @@ -33,14 +33,29 @@ Kotlin:: ---- val parser = SpelExpressionParser() - val name = parser.parseExpression("name?:'Unknown'").getValue(Inventor(), String::class.java) + val name = parser.parseExpression("name ?: 'Unknown'").getValue(Inventor(), String::class.java) println(name) // 'Unknown' ---- ====== -NOTE: The SpEL Elvis operator also checks for _empty_ Strings in addition to `null` objects. -The original snippet is thus only close to emulating the semantics of the operator (it would need an -additional `!name.isEmpty()` check). +[NOTE] +==== +The SpEL Elvis operator also treats an _empty_ String like a `null` object. Thus, the +original Java example is only close to emulating the semantics of the operator: it would +need to use `name != null && !name.isEmpty()` as the predicate to be compatible with the +semantics of the SpEL Elvis operator. +==== + +[TIP] +==== +As of Spring Framework 7.0, the SpEL Elvis operator supports `java.util.Optional` with +transparent unwrapping semantics. + +For example, given the expression `A ?: B`, if `A` is `null` or an _empty_ `Optional`, +the expression evaluates to `B`. However, if `A` is a non-empty `Optional` the expression +evaluates to the object contained in the `Optional`, thereby effectively unwrapping the +`Optional` which correlates to `A.get()`. +==== The following listing shows a more complex example: @@ -54,11 +69,11 @@ Java:: EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding().build(); Inventor tesla = new Inventor("Nikola Tesla", "Serbian"); - String name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); + String name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class); System.out.println(name); // Nikola Tesla tesla.setName(""); - name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String.class); + name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String.class); System.out.println(name); // Elvis Presley ---- @@ -70,16 +85,16 @@ Kotlin:: val context = SimpleEvaluationContext.forReadOnlyDataBinding().build() val tesla = Inventor("Nikola Tesla", "Serbian") - var name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) + var name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java) println(name) // Nikola Tesla tesla.setName("") - name = parser.parseExpression("name?:'Elvis Presley'").getValue(context, tesla, String::class.java) + name = parser.parseExpression("name ?: 'Elvis Presley'").getValue(context, tesla, String::class.java) println(name) // Elvis Presley ---- ====== -[NOTE] +[TIP] ===== You can use the Elvis operator to apply default values in expressions. The following example shows how to use the Elvis operator in a `@Value` expression: @@ -89,7 +104,6 @@ example shows how to use the Elvis operator in a `@Value` expression: @Value("#{systemProperties['pop3.port'] ?: 25}") ---- -This will inject a system property `pop3.port` if it is defined or 25 if not. +This will inject the value of the system property named `pop3.port` if it is defined or +`25` if the property is not defined. ===== - - diff --git a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc index 60d406cb2963..2ab7a5498186 100644 --- a/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc +++ b/framework-docs/modules/ROOT/pages/core/expressions/language-ref/operator-safe-navigation.adoc @@ -252,7 +252,6 @@ Kotlin:: <1> Use "null-safe select first" operator on potentially null `members` list ====== - The following example shows how to use the "null-safe select last" operator for collections (`?.$`). @@ -351,6 +350,50 @@ Kotlin:: <2> Use null-safe projection operator on null `members` list ====== +[[expressions-operator-safe-navigation-optional]] +== Null-safe Operations on `Optional` + +As of Spring Framework 7.0, null-safe operations are supported on instances of +`java.util.Optional` with transparent unwrapping semantics. + +Specifically, when a null-safe operator is applied to an _empty_ `Optional`, it will be +treated as if the `Optional` were `null`, and the subsequent operation will evaluate to +`null`. However, if a null-safe operator is applied to a non-empty `Optional`, the +subsequent operation will be applied to the object contained in the `Optional`, thereby +effectively unwrapping the `Optional`. + +For example, if `user` is of type `Optional`, the expression `user?.name` will +evaluate to `null` if `user` is either `null` or an _empty_ `Optional` and will otherwise +evaluate to the `name` of the `user`, effectively `user.get().getName()` or +`user.get().name` for property or field access, respectively. + +[NOTE] +==== +Invocations of methods defined in the `Optional` API are still supported on an _empty_ +`Optional`. For example, if `name` is of type `Optional`, the expression +`name?.orElse('Unknown')` will evaluate to `"Unknown"` if `name` is an empty `Optional` +and will otherwise evaluate to the `String` contained in the `Optional` if `name` is a +non-empty `Optional`, effectively `name.get()`. +==== + +// NOTE: ⁠ is the Unicode Character 'WORD JOINER', which prevents undesired line wraps. + +Similarly, if `names` is of type `Optional>`, the expression +`names?.?⁠[#this.length > 5]` will evaluate to `null` if `names` is `null` or an _empty_ +`Optional` and will otherwise evaluate to a sequence containing the names whose lengths +are greater than 5, effectively +`names.get().stream().filter(s -> s.length() > 5).toList()`. + +The same semantics apply to all of the null-safe operators mentioned previously in this +chapter. + +For further details and examples, consult the javadoc for the following operators. + +* {spring-framework-api}/expression/spel/ast/PropertyOrFieldReference.html[`PropertyOrFieldReference`] +* {spring-framework-api}/expression/spel/ast/MethodReference.html[`MethodReference`] +* {spring-framework-api}/expression/spel/ast/Indexer.html[`Indexer`] +* {spring-framework-api}/expression/spel/ast/Selection.html[`Selection`] +* {spring-framework-api}/expression/spel/ast/Projection.html[`Projection`] [[expressions-operator-safe-navigation-compound-expressions]] == Null-safe Operations in Compound Expressions diff --git a/framework-docs/modules/ROOT/pages/core/null-safety.adoc b/framework-docs/modules/ROOT/pages/core/null-safety.adoc index 8e2fe8ed42be..23355b6bb3e8 100644 --- a/framework-docs/modules/ROOT/pages/core/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/core/null-safety.adoc @@ -1,57 +1,187 @@ [[null-safety]] = Null-safety -Although Java does not let you express null-safety with its type system, the Spring Framework -provides the following annotations in the `org.springframework.lang` package to let you -declare nullability of APIs and fields: +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 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]. -* {spring-framework-api}/lang/Nullable.html[`@Nullable`]: Annotation to indicate that a -specific parameter, return value, or field can be `null`. -* {spring-framework-api}/lang/NonNull.html[`@NonNull`]: Annotation to indicate that a specific -parameter, return value, or field cannot be `null` (not needed on parameters, return values, -and fields where `@NonNullApi` and `@NonNullFields` apply, respectively). -* {spring-framework-api}/lang/NonNullApi.html[`@NonNullApi`]: Annotation at the package level -that declares non-null as the default semantics for parameters and return values. -* {spring-framework-api}/lang/NonNullFields.html[`@NonNullFields`]: Annotation at the package -level that declares non-null as the default semantics for fields. +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, and Java primitive types, as well as a pragmatic check on any `@Nullable` annotation (regardless of the +package). -The Spring Framework itself leverages these annotations, but they can also be used in any -Spring-based Java project to declare null-safe APIs and optionally null-safe fields. -Nullability declarations for generic type arguments, varargs, and array elements are not supported yet. -Nullability declarations are expected to be fine-tuned between Spring Framework releases, -including minor ones. Nullability of types used inside method bodies is outside the -scope of this feature. +[[null-safety-libraries]] +== Annotating libraries with JSpecify annotations -NOTE: Other common libraries such as Reactor and Spring Data provide null-safe APIs that -use a similar nullability arrangement, delivering a consistent overall experience for -Spring application developers. +As of Spring Framework 7, the Spring Framework codebase leverages JSpecify annotations to expose null-safe APIs and +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 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 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 proposed guidelines for explicitly specifying the nullability of +Spring-related libraries or applications. + + +[[null-safety-guidelines-jspecify]] +=== JSpecify + +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"] +---- +@NullMarked +package org.springframework.core; + +import org.jspecify.annotations.NullMarked; +---- + +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 on the same line. + +For example, for a field: + +[source,java,subs="verbatim,quotes"] +---- +private @Nullable String fileEncoding; +---- + +Or for method parameters and method return types: + +[source,java,subs="verbatim,quotes"] +---- +public static @Nullable String buildMessage(@Nullable String message, + @Nullable Throwable cause) { + // ... +} +---- + +[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 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 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` + - `jakarta.validation.@Nullable Validator` + +https://jspecify.dev/docs/api/org/jspecify/annotations/NonNull.html[`@NonNull`] and +https://jspecify.dev/docs/api/org/jspecify/annotations/NullUnmarked.html[`@NullUnmarked`] should rarely be needed for +typical use cases. + +[[null-safety-guidelines-nullaway]] +=== NullAway + +==== Configuration + +The recommended configuration is: + + - `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 irrelevant warnings in your codebase. + +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 +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 previously in this section. + +==== Warnings suppression + +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 +{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 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 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]] +== Migrating from Spring null-safety annotations + +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 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 issues, better tooling, better Kotlin integration, +and the capability to specify nullability more precisely for more use cases. + +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 semantics. For example +`@Nullable Object[] array` with Spring annotations needs to be changed to `Object @Nullable [] array` with JSpecify +annotations. The same applies to varargs. + +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 method return types, instead of `@Nullable public String method()` with Spring annotations, use +`public @Nullable String method()` with JSpecify annotations. + +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. -[[use-cases]] -== Use cases - -In addition to providing an explicit declaration for Spring Framework API nullability, -these annotations can be used by an IDE (such as IDEA or Eclipse) to provide useful -warnings related to null-safety in order to avoid `NullPointerException` at runtime. - -They are also used to make Spring APIs null-safe in Kotlin projects, since Kotlin natively -supports {kotlin-docs}/null-safety.html[null-safety]. More details -are available in the xref:languages/kotlin/null-safety.adoc[Kotlin support documentation]. - - - - -[[jsr-305-meta-annotations]] -== JSR-305 meta-annotations - -Spring annotations are meta-annotated with {JSR}305[JSR 305] -annotations (a dormant but widespread JSR). JSR-305 meta-annotations let tooling vendors -like IDEA or Kotlin provide null-safety support in a generic way, without having to -hard-code support for Spring annotations. - -It is neither necessary nor recommended to add a JSR-305 dependency to the project classpath to -take advantage of Spring's null-safe APIs. Only projects such as Spring-based libraries that use -null-safety annotations in their codebase should add `com.google.code.findbugs:jsr305:3.0.2` -with `compileOnly` Gradle configuration or Maven `provided` scope to avoid compiler warnings. diff --git a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc b/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc deleted file mode 100644 index 547b80ddd435..000000000000 --- a/framework-docs/modules/ROOT/pages/core/spring-jcl.adoc +++ /dev/null @@ -1,47 +0,0 @@ -[[spring-jcl]] -= Logging - -Spring comes with its own Commons Logging bridge implemented -in the `spring-jcl` module. The implementation checks for the presence of the Log4j 2.x -API and the SLF4J 1.7 API in the classpath and uses the first one of those found as the -logging implementation, falling back to the Java platform's core logging facilities (also -known as _JUL_ or `java.util.logging`) if neither Log4j 2.x nor SLF4J is available. - -Put Log4j 2.x or Logback (or another SLF4J provider) in your classpath, without any extra -bridges, and let the framework auto-adapt to your choice. For further information see the -{spring-boot-docs-ref}/features/logging.html[Spring -Boot Logging Reference Documentation]. - -[NOTE] -==== -Spring's Commons Logging variant is only meant to be used for infrastructure logging -purposes in the core framework and in extensions. - -For logging needs within application code, prefer direct use of Log4j 2.x, SLF4J, or JUL. -==== - -A `Log` implementation may be retrieved via `org.apache.commons.logging.LogFactory` as in -the following example. - -[tabs] -====== -Java:: -+ -[source,java,indent=0,subs="verbatim,quotes"] ----- -public class MyBean { - private final Log log = LogFactory.getLog(getClass()); - // ... -} ----- - -Kotlin:: -+ -[source,kotlin,indent=0,subs="verbatim,quotes"] ----- -class MyBean { - private val log = LogFactory.getLog(javaClass) - // ... -} ----- -====== diff --git a/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc b/framework-docs/modules/ROOT/pages/core/validation/beanvalidation.adoc index 5d087e564185..f5d83d4ad705 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 96a6023dac51..83ccd98d84bd 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 { - - @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); - } - }; - } + @Configuration + public class DataSourceConfig { + + @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); + } + }; + } } ---- diff --git a/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc b/framework-docs/modules/ROOT/pages/data-access/r2dbc.adoc index c27fd7ec4519..086562d73bdd 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 000000000000..8ef9878fa352 --- /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 aeffe326c10d..000000000000 --- 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/email.adoc b/framework-docs/modules/ROOT/pages/integration/email.adoc index 46493a7de9a0..420bc481c9d8 100644 --- a/framework-docs/modules/ROOT/pages/integration/email.adoc +++ b/framework-docs/modules/ROOT/pages/integration/email.adoc @@ -11,9 +11,7 @@ Spring Framework's email support: * The https://jakartaee.github.io/mail-api/[Jakarta Mail] library This library is freely available on the web -- for example, in Maven Central as -`com.sun.mail:jakarta.mail`. Please make sure to use the latest 2.x version (which uses -the `jakarta.mail` package namespace) rather than Jakarta Mail 1.6.x (which uses the -`javax.mail` package namespace). +`org.eclipse.angus:angus-mail`. **** The Spring Framework provides a helpful utility library for sending email that shields diff --git a/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc b/framework-docs/modules/ROOT/pages/integration/rest-clients.adoc index d6a143eab1e9..057cee0d2c2c 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 + +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. -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. -Start by creating the interface with `@HttpExchange` methods: +First, create the Java interface: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -867,69 +881,64 @@ Start by creating the interface with `@HttpExchange` methods: } ---- -Now you can create a proxy that performs requests when methods are called. - -For `RestClient`: +Optionally, use `@HttpExchange` at the type level to declare common attributes for all methods: [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(); + @HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") + public interface RepositoryService { - RepositoryService service = factory.createClient(RepositoryService.class); ----- + @GetExchange + Repository getRepository(@PathVariable String owner, @PathVariable String repo); -For `WebClient`: + @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) + void updateRepository(@PathVariable String owner, @PathVariable String repo, + @RequestParam String name, @RequestParam String description, @RequestParam String homepage); -[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`: +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(); - 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); + HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); ---- -`@HttpExchange` is supported at the type level where it applies to all methods: +Now, you're ready to create client proxies: [source,java,indent=0,subs="verbatim,quotes"] ---- - @HttpExchange(url = "/repos/{owner}/{repo}", accept = "application/vnd.github.v3+json") - public interface RepositoryService { - - @GetExchange - Repository getRepository(@PathVariable String owner, @PathVariable String repo); - - @PatchExchange(contentType = MediaType.APPLICATION_FORM_URLENCODED_VALUE) - void updateRepository(@PathVariable String owner, @PathVariable String repo, - @RequestParam String name, @RequestParam String description, @RequestParam String homepage); - - } + 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 -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. +[[rest-http-interface.custom-resolver]] +=== Custom Arguments -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`: +[[rest-http-interface-exceptions]] +=== Error Handling -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); + + // or for WebClient... + WebClient webClient = WebClient.builder() + .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) + .build(); + WebClientAdapter adapter = WebClientAdapter.create(webClient); + + // 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 `RestClient.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`. + + + +[[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. -For `WebClient`: +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. -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: +One way to declare HTTP Service groups is via `@ImportHttpServices` annotations in +`@Configuration` classes as shown below: [source,java,indent=0,subs="verbatim,quotes"] ---- - WebClient webClient = WebClient.builder() - .defaultStatusHandler(HttpStatusCode::isError, resp -> ...) - .build(); + @Configuration + @ImportHttpServices(group = "echo", types = {EchoServiceA.class, EchoServiceB.class}) // <1> + @ImportHttpServices(group = "greeting", basePackageClasses = GreetServiceA.class) // <2> + public class ClientConfig { + } - WebClientAdapter adapter = WebClientAdapter.create(webClient); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder(adapter).build(); ---- +<1> Manually list interfaces for group "echo" +<2> Detect interfaces for group "greeting" under a base package -For more details and options, such as suppressing error status codes, see the Javadoc of -`defaultStatusHandler` in `WebClient.Builder`. +It is also possible to declare groups programmatically by creating an HTTP Service +registrar and then importing it: -For `RestTemplate`: +[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) -> ...); + }; + } + } +---- + +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. -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: +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"] ---- - RestTemplate restTemplate = new RestTemplate(); - restTemplate.setErrorHandler(myErrorHandler); + @RestController + public class EchoController { + + private final EchoService echoService; + + public EchoController(EchoService echoService) { + this.echoService = echoService; + } - RestTemplateAdapter adapter = RestTemplateAdapter.create(restTemplate); - HttpServiceProxyFactory factory = HttpServiceProxyFactory.builderFor(adapter).build(); + // ... + } ---- -For more details and options, see the Javadoc of `setErrorHandler` in `RestTemplate` and -the `ResponseErrorHandler` hierarchy. +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/bean-definition-dsl.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc index c2c2b9f246da..6b1752efbc26 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/bean-definition-dsl.adoc @@ -1,113 +1,7 @@ [[kotlin-bean-definition-dsl]] = Bean Definition DSL -Spring Framework supports registering beans in a functional way by using lambdas -as an alternative to XML or Java configuration (`@Configuration` and `@Bean`). In a nutshell, -it lets you register beans with a lambda that acts as a `FactoryBean`. -This mechanism is very efficient, as it does not require any reflection or CGLIB proxies. - -In Java, you can, for example, write the following: - -[source,java,indent=0] ----- - class Foo {} - - class Bar { - private final Foo foo; - public Bar(Foo foo) { - this.foo = foo; - } - } - - GenericApplicationContext context = new GenericApplicationContext(); - context.registerBean(Foo.class); - context.registerBean(Bar.class, () -> new Bar(context.getBean(Foo.class))); ----- - -In Kotlin, with reified type parameters and `GenericApplicationContext` Kotlin extensions, -you can instead write the following: - -[source,kotlin,indent=0] ----- - class Foo - - class Bar(private val foo: Foo) - - val context = GenericApplicationContext().apply { - registerBean() - registerBean { Bar(it.getBean()) } - } ----- - -When the class `Bar` has a single constructor, you can even just specify the bean class, -the constructor parameters will be autowired by type: - -[source,kotlin,indent=0] ----- - val context = GenericApplicationContext().apply { - registerBean() - registerBean() - } ----- - -In order to allow a more declarative approach and cleaner syntax, Spring Framework provides -a {spring-framework-api-kdoc}/spring-context/org.springframework.context.support/-bean-definition-dsl/index.html[Kotlin bean definition DSL] -It declares an `ApplicationContextInitializer` through a clean declarative API, -which lets you deal with profiles and `Environment` for customizing -how beans are registered. - -In the following example notice that: - -* Type inference usually allows to avoid specifying the type for bean references like `ref("bazBean")` -* It is possible to use Kotlin top level functions to declare beans using callable references like `bean(::myRouter)` in this example -* When specifying `bean()` or `bean(::myRouter)`, parameters are autowired by type -* The `FooBar` bean will be registered only if the `foobar` profile is active - -[source,kotlin,indent=0] ----- - class Foo - class Bar(private val foo: Foo) - class Baz(var message: String = "") - class FooBar(private val baz: Baz) - - val myBeans = beans { - bean() - bean() - bean("bazBean") { - Baz().apply { - message = "Hello world" - } - } - profile("foobar") { - bean { FooBar(ref("bazBean")) } - } - bean(::myRouter) - } - - fun myRouter(foo: Foo, bar: Bar, baz: Baz) = router { - // ... - } ----- - -NOTE: This DSL is programmatic, meaning it allows custom registration logic of beans -through an `if` expression, a `for` loop, or any other Kotlin constructs. - -You can then use this `beans()` function to register beans on the application context, -as the following example shows: - -[source,kotlin,indent=0] ----- - val context = GenericApplicationContext().apply { - myBeans.initialize(this) - refresh() - } ----- - -NOTE: Spring Boot is based on JavaConfig and -{spring-boot-issues}/8115[does not yet provide specific support for functional bean definition], -but you can experimentally use functional bean definitions through Spring Boot's `ApplicationContextInitializer` support. -See {stackoverflow-questions}/45935931/how-to-use-functional-bean-definition-kotlin-dsl-with-spring-boot-and-spring-w/46033685#46033685[this Stack Overflow answer] -for more details and up-to-date information. See also the experimental Kofu DSL developed in {spring-github-org}-experimental/spring-fu[Spring Fu incubator]. +See xref:core/beans/java/programmatic-bean-registration.adoc[Programmatic Bean Registration]. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/coroutines.adoc index 9e09a69411ab..6893fb5a48c6 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/null-safety.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc index 96070d163f42..213a04c9dfa6 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/null-safety.adoc @@ -5,34 +5,11 @@ One of Kotlin's key features is {kotlin-docs}/null-safety.html[null-safety], which cleanly deals with `null` values at compile time rather than bumping into the famous `NullPointerException` at runtime. This makes applications safer through nullability declarations and expressing "`value or no value`" semantics without paying the cost of wrappers, such as `Optional`. -(Kotlin allows using functional constructs with nullable values. See this -{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety].) +Kotlin allows using functional constructs with nullable values. See this +{baeldung-blog}/kotlin-null-safety[comprehensive guide to Kotlin null-safety]. Although Java does not let you express null-safety in its type-system, the Spring Framework -provides xref:languages/kotlin/null-safety.adoc[null-safety of the whole Spring Framework API] -via tooling-friendly annotations declared in the `org.springframework.lang` package. -By default, types from Java APIs used in Kotlin are recognized as -{kotlin-docs}/java-interop.html#null-safety-and-platform-types[platform types], -for which null-checks are relaxed. -{kotlin-docs}/java-interop.html#jsr-305-support[Kotlin support for JSR-305 annotations] -and Spring nullability annotations provide null-safety for the whole Spring Framework API to Kotlin developers, -with the advantage of dealing with `null`-related issues at compile time. - -NOTE: Libraries such as Reactor or Spring Data provide null-safe APIs to leverage this feature. - -You can configure JSR-305 checks by adding the `-Xjsr305` compiler flag with the following -options: `-Xjsr305={strict|warn|ignore}`. - -For kotlin versions 1.1+, the default behavior is the same as `-Xjsr305=warn`. -The `strict` value is required to have Spring Framework API null-safety taken into account -in Kotlin types inferred from Spring API but should be used with the knowledge that Spring -API nullability declaration could evolve even between minor releases and that more checks may -be added in the future. - -NOTE: Generic type arguments, varargs, and array elements nullability are not supported yet, -but should be in an upcoming release. See {kotlin-github-org}/KEEP/issues/79[this discussion] -for up-to-date information. - - - +provides xref:core/null-safety.adoc[null-safety of the whole Spring Framework API] +via tooling-friendly https://jspecify.dev/[JSpecify] annotations. +As of Kotlin 2.1, Kotlin enforces strict handling of nullability annotations from `org.jspecify.annotations` package. diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc index d2b3657d3127..fcc26798b546 100644 --- a/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc +++ b/framework-docs/modules/ROOT/pages/languages/kotlin/requirements.adoc @@ -2,7 +2,7 @@ = Requirements :page-section-summary-toc: 1 -Spring Framework supports Kotlin 1.7+ and requires +Spring Framework supports Kotlin 2.1+ and requires https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-stdlib[`kotlin-stdlib`] and https://search.maven.org/artifact/org.jetbrains.kotlin/kotlin-reflect[`kotlin-reflect`] to be present on the classpath. They are provided by default if you bootstrap a Kotlin project on 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 64da5a0b63ed..90b0d6fb0d94 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") - - @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() - } - - @AfterAll - fun afterAll() { - application.stop() - } + val application = Application(8181) + val client = WebClient.create("http://localhost:8181") + + @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() + } + + @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 { - - @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) - } - } -} + class SpecificationLikeTests { + + @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) + } + } + } ---- diff --git a/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc b/framework-docs/modules/ROOT/pages/languages/kotlin/web.adoc index e594069b0d4a..a1aeafd5e58f 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,86 +53,33 @@ 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 - } - principal = Principal { "foo" } -}.andExpect { - status { isOk } - content { contentType(APPLICATION_JSON) } - jsonPath("$.name") { value("Lee") } - content { json("""{"someBoolean": false}""", false) } -}.andDo { - print() -} ----- - - - -[[kotlin-script-templates]] -== Kotlin Script Templates - -Spring Framework provides a -{spring-framework-api}/web/servlet/view/script/ScriptTemplateView.html[`ScriptTemplateView`] -which supports {JSR}223[JSR-223] to render templates by using script engines. - -By leveraging `scripting-jsr223` dependencies, it -is possible to use such feature to render Kotlin-based templates with -{kotlin-github-org}/kotlinx.html[kotlinx.html] DSL or Kotlin multiline interpolated `String`. - -`build.gradle.kts` -[source,kotlin,indent=0] ----- -dependencies { - runtime("org.jetbrains.kotlin:kotlin-scripting-jsr223:${kotlinVersion}") -} ----- - -Configuration is usually done with `ScriptTemplateConfigurer` and `ScriptTemplateViewResolver` beans. - -`KotlinScriptConfiguration.kt` -[source,kotlin,indent=0] ----- -@Configuration -class KotlinScriptConfiguration { - - @Bean - fun kotlinScriptConfigurer() = ScriptTemplateConfigurer().apply { - engineName = "kotlin" - setScripts("scripts/render.kts") - renderFunction = "render" - isSharedEngine = false + 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() } - - @Bean - fun kotlinScriptViewResolver() = ScriptTemplateViewResolver().apply { - setPrefix("templates/") - setSuffix(".kts") - } -} ---- -See the https://github.com/sdeleuze/kotlin-script-templating[kotlin-script-templating] example -project for more details. - [[kotlin-multiplatform-serialization]] == Kotlin multiplatform serialization {kotlin-github-org}/kotlinx.serialization[Kotlin multiplatform serialization] is -supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, and ProtoBuf formats. - -To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependency and plugin. -With Spring MVC and WebFlux, both Kotlin serialization and Jackson will be configured by default if they are in the classpath since -Kotlin serialization is designed to serialize only Kotlin classes annotated with `@Serializable`. -With Spring Messaging (RSocket), make sure that neither Jackson, GSON or JSONB are in the classpath if you want automatic configuration, -if Jackson is needed configure `KotlinSerializationJsonMessageConverter` manually. - - - +supported in Spring MVC, Spring WebFlux and Spring Messaging (RSocket). The builtin support currently targets CBOR, JSON, +and ProtoBuf formats. +To enable it, follow {kotlin-github-org}/kotlinx.serialization#setup[those instructions] to add the related dependencies +and plugin. With Spring MVC and WebFlux, Kotlin serialization is configured by default if it is in the classpath and +other variants like Jackson are not. If needed, configure the converters or codecs manually. 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 d359c708ea70..5ab998f9693d 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`] @@ -171,9 +171,9 @@ the parameters of a test class constructor are autowired from components in the If `@TestConstructor` is not present or meta-present on a test class, the default _test constructor autowire mode_ will be used. See the tip below for details on how to change -the default mode. Note, however, that a local declaration of `@Autowired`, -`@jakarta.inject.Inject`, or `@javax.inject.Inject` on a constructor takes precedence -over both `@TestConstructor` and the default mode. +the default mode. Note, however, that a local declaration of `@Autowired` or +`@jakarta.inject.Inject` on a constructor takes precedence over both `@TestConstructor` +and the default mode. .Changing the default test constructor autowire mode [TIP] 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 17fdf5c9bef0..f7e41bb4f64d 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 ce2381c7c1c5..a956a7c7bc78 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 153a3b03435f..f92d5c584afa 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 a9cc9ced52dc..4ec33c0c154f 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 949b9ab8a9bd..f6f5d9f14f18 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 175af0f55eec..d3a8df109d38 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 55738c214f7d..b145277f21c8 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 0cf24faa9aa1..13de70cdef32 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 a75d6314aab7..cec19b9185e3 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 1698c6169291..f1af4efc3c9f 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 c8d57c4276cb..22f97cc1a0a7 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 967f774288f5..44613c348322 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 6e3c268f639b..c95363d9462a 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 1ee5856ecfd0..71f9da8acdce 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/testcontext-framework/web-scoped-beans.adoc b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/web-scoped-beans.adoc index f716f1d9612b..6be937f4f22b 100644 --- a/framework-docs/modules/ROOT/pages/testing/testcontext-framework/web-scoped-beans.adoc +++ b/framework-docs/modules/ROOT/pages/testing/testcontext-framework/web-scoped-beans.adoc @@ -96,9 +96,7 @@ Kotlin:: The following code snippet is similar to the one we saw earlier for a request-scoped bean. However, this time, the `userService` bean has a dependency on a session-scoped `userPreferences` bean. Note that the `UserPreferences` bean is instantiated by using a -SpEL expression that retrieves the theme from the current HTTP session. In our test, we -need to configure a theme in the mock session managed by the TestContext framework. The -following example shows how to do so: +SpEL expression that retrieves an attribute from the current HTTP session. .Session-scoped bean configuration [source,xml,indent=0,subs="verbatim,quotes"] diff --git a/framework-docs/modules/ROOT/pages/testing/unit.adoc b/framework-docs/modules/ROOT/pages/testing/unit.adoc index 249e58c07b7c..ddc70fd18f8e 100644 --- a/framework-docs/modules/ROOT/pages/testing/unit.adoc +++ b/framework-docs/modules/ROOT/pages/testing/unit.adoc @@ -47,8 +47,7 @@ out-of-container tests for code that depends on environment-specific properties. The `org.springframework.mock.web` package contains a comprehensive set of Servlet API mock objects that are useful for testing web contexts, controllers, and filters. These mock objects are targeted at usage with Spring's Web MVC framework and are generally more -convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock]) -or alternative Servlet API mock objects (such as http://www.mockobjects.com[MockObjects]). +convenient to use than dynamic mock objects (such as https://easymock.org/[EasyMock]). TIP: Since Spring Framework 6.0, the mock objects in `org.springframework.mock.web` are based on the Servlet 6.0 API. diff --git a/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc b/framework-docs/modules/ROOT/pages/testing/webtestclient.adoc index 2642be67edf7..ae3bd4f61044 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-functional.adoc b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc index f0aaea966b55..ae6368f8d309 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-functional.adoc @@ -804,8 +804,7 @@ Java:: [source,java,indent=0,subs="verbatim,quotes"] ---- ClassPathResource index = new ClassPathResource("static/index.html"); - List extensions = List.of("js", "css", "ico", "png", "jpg", "gif"); - RequestPredicate spaPredicate = path("/api/**").or(path("/error")).or(pathExtension(extensions::contains)).negate(); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); RouterFunction redirectToIndex = route() .resource(spaPredicate, index) .build(); @@ -817,9 +816,7 @@ Kotlin:: ---- val redirectToIndex = router { val index = ClassPathResource("static/index.html") - val extensions = listOf("js", "css", "ico", "png", "jpg", "gif") - val spaPredicate = !(path("/api/**") or path("/error") or - pathExtension(extensions::contains)) + val spaPredicate = !(path("/api/**") or path("/error")) resource(spaPredicate, index) } ---- 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 000000000000..c05bac73c0e5 --- /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-view.adoc b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc index dca3a958c6f8..eb64696b1d6e 100644 --- a/framework-docs/modules/ROOT/pages/web/webflux-view.adoc +++ b/framework-docs/modules/ROOT/pages/web/webflux-view.adoc @@ -225,9 +225,9 @@ The following table shows the templating libraries that we have tested on differ |Scripting Library |Scripting Engine |https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn] |https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn] -|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn] -|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn] -|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby] +|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn] +|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn] +|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby] |https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython] |https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== @@ -467,7 +467,7 @@ Java:: ---- @GetMapping FragmentsRendering handle() { - return FragmentsRendering.with("posts").fragment("comments").build(); + return FragmentsRendering.fragment("posts").fragment("comments").build(); } ---- @@ -477,7 +477,7 @@ Kotlin:: ---- @GetMapping fun handle(): FragmentsRendering { - return FragmentsRendering.with("posts").fragment("comments").build() + return FragmentsRendering.fragment("posts").fragment("comments").build() } ---- ====== 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 bd52881b4591..7ff92ff268d6 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 53a2fc247cb8..c6ce0f086c38 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 d63ed06a9fb8..a2d4ad961f86 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)) - } - } - } -} ----- -====== \ No newline at end of file + 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)) + } + } + } + } +---- +====== 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 febbb5498272..f7a69c7dbb3d 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 1e7f397c19b6..04ea4fa3869b 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 15f15e7115db..f2a75ba80190 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); - } + } } ---- @@ -647,13 +647,12 @@ For https://www.webjars.org/documentation[WebJars], versioned URLs like `/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them. The related resource location is configured out of the box with Spring Boot (or can be configured manually via `ResourceHandlerRegistry`) and does not require to add the -`org.webjars:webjars-locator-core` dependency. +`org.webjars:webjars-locator-lite` dependency. Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the `WebJarsResourceResolver` which is automatically registered when the -`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a -classpath scanning that could slow down application startup. The resolver can re-write URLs to -include the version of the jar and can also match against incoming URLs without versions +`org.webjars:webjars-locator-lite` library is present on the classpath. The resolver can re-write +URLs to include the version of the jar and can also match against incoming URLs without versions -- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`. TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options @@ -687,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 24999a0e1726..632377c7b970 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 5616e8110b8f..ab6c1f647ec8 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 22fe3570b6c6..450b9e99258c 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 7d79ea1d468e..f4a611010593 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 cbfcad7a7cec..295a16a264e0 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 53ff9683c69e..553b9d9dac21 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 @@ -57,7 +57,7 @@ locale and language specific resource bundles. For further custom handling of method validation errors, you can extend `ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly. -The exception contains a list of``ParameterValidationResult``s that group validation errors +The exception contains a list of ``ParameterValidationResult``s that group validation errors by method parameter. You can either iterate over those, or provide a visitor with callback methods by controller method parameter type: @@ -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 6e980e5197e4..13e4c4b0d4f1 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 c56df6c842cb..497f746b5b45 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,9 +781,9 @@ 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")).or(pathExtension(extensions::contains)).negate(); + RequestPredicate spaPredicate = path("/api/**").or(path("/error")).negate(); RouterFunction redirectToIndex = route() .resource(spaPredicate, index) .build(); @@ -793,11 +793,9 @@ Kotlin:: + [source,kotlin,indent=0,subs="verbatim,quotes"] ---- - val redirectToIndex = router { + val redirectToIndex = router { val index = ClassPathResource("static/index.html") - val extensions = listOf("js", "css", "ico", "png", "jpg", "gif") - val spaPredicate = !(path("/api/**") or path("/error") or - pathExtension(extensions::contains)) + val spaPredicate = !(path("/api/**") or path("/error")) resource(spaPredicate, index) } ---- @@ -814,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) } ---- ====== 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 000000000000..7ea6fad3ecc4 --- /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-view/mvc-fragments.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc index 45e4a57adc4e..953883659a9c 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-fragments.adoc @@ -48,7 +48,7 @@ Java:: ---- @GetMapping FragmentsRendering handle() { - return FragmentsRendering.with("posts").fragment("comments").build(); + return FragmentsRendering.fragment("posts").fragment("comments").build(); } ---- @@ -58,7 +58,7 @@ Kotlin:: ---- @GetMapping fun handle(): FragmentsRendering { - return FragmentsRendering.with("posts").fragment("comments").build() + return FragmentsRendering.fragment("posts").fragment("comments").build() } ---- ====== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc index 5329bc919cf6..3bb07dfbe067 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc-view/mvc-script.adoc @@ -13,9 +13,9 @@ templating libraries on different script engines: |Scripting Library |Scripting Engine |https://handlebarsjs.com/[Handlebars] |https://openjdk.java.net/projects/nashorn/[Nashorn] |https://mustache.github.io/[Mustache] |https://openjdk.java.net/projects/nashorn/[Nashorn] -|https://facebook.github.io/react/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn] -|https://www.embeddedjs.com/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn] -|https://www.stuartellis.name/articles/erb/[ERB] |https://www.jruby.org[JRuby] +|https://react.dev/[React] |https://openjdk.java.net/projects/nashorn/[Nashorn] +|https://ejs.co/[EJS] |https://openjdk.java.net/projects/nashorn/[Nashorn] +|https://docs.ruby-lang.org/en/master/ERB.html[ERB] |https://www.jruby.org[JRuby] |https://docs.python.org/2/library/string.html#template-strings[String templates] |https://www.jython.org/[Jython] |https://github.com/sdeleuze/kotlin-script-templating[Kotlin Script templating] |{kotlin-site}[Kotlin] |=== diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc index ba61dc917b85..9bd13191aec1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/filters.adoc @@ -118,12 +118,12 @@ See the sections on xref:web/webmvc-cors.adoc[CORS] and the xref:web/webmvc-cors [.small]#xref:web/webflux/reactive-spring.adoc#filters.url-handler[See equivalent in the Reactive stack]# In previous Spring Framework versions, Spring MVC could be configured to ignore trailing slashes in URL paths -when mapping incoming requests on controller methods. This could be done by enabling the `setUseTrailingSlashMatch` -option on the `PathMatchConfigurer`. This means that sending a "GET /home/" request would be handled by a controller -method annotated with `@GetMapping("/home")`. +when mapping incoming requests on controller methods. This means that sending a "GET /home/" request would be +handled by a controller method annotated with `@GetMapping("/home")`. -This option has been retired, but applications are still expected to handle such requests in a safe way. -The `UrlHandlerFilter` Servlet filter has been designed for this purpose. It can be configured to: +This option was deprecated in 6.0 and removed in 7.0, but applications are still expected to handle such +requests in a safe way. The `UrlHandlerFilter` Servlet filter has been designed for this purpose. +It can be configured to: * respond with an HTTP redirect status when receiving URLs with trailing slashes, sending browsers to the non-trailing slash URL variant. * wrap the request to act as if the request was sent without a trailing slash and continue the processing of the request. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc index 2b86c30d5309..fc6d5a184761 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/message-converters.adoc @@ -54,6 +54,11 @@ You can customize XML mapping as needed through the use of JAXB or Jackson's pro When you need further control (for cases where custom XML serializers/deserializers need to be provided for specific types), you can inject a custom `XmlMapper` through the `ObjectMapper` property. By default, this converter supports `application/xml`. This requires the `com.fasterxml.jackson.dataformat:jackson-dataformat-xml` dependency. +| `KotlinSerializationJsonHttpMessageConverter` +| An `HttpMessageConverter` implementation that can read and write JSON using `kotlinx.serialization`. +This converter is not configured by default, as this conflicts with Jackson. +Developers must configure it as an additional converter ahead of the Jackson one. + | `MappingJackson2CborHttpMessageConverter` | `com.fasterxml.jackson.dataformat:jackson-dataformat-cbor` 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 9e248d5f24ce..3f71ba485252 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 000000000000..88ffa09a6a11 --- /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-config/enable.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc index b5beda90a007..f8dde6657667 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/enable.adoc @@ -7,6 +7,10 @@ You can use the `@EnableWebMvc` annotation to enable MVC configuration with prog include-code::./WebConfiguration[tag=snippet,indent=0] +WARNING: As of 7.0, support for the XML configuration namespace for Spring MVC has been deprecated. +There are no plans yet for removing it completely but XML configuration will not be updated to follow +the Java configuration model. + NOTE: When using Spring Boot, you may want to use `@Configuration` classes of type `WebMvcConfigurer` but without `@EnableWebMvc` to keep Spring Boot MVC customizations. See more details in xref:web/webmvc/mvc-config/customize.adoc[the MVC Config API section] and in {spring-boot-docs-ref}/web/servlet.html#web.servlet.spring-mvc.auto-configuration[the dedicated Spring Boot documentation]. The preceding example registers a number of Spring MVC diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc index 362adf674df0..87545bbadbd6 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-config/static-resources.adoc @@ -48,13 +48,12 @@ For https://www.webjars.org/documentation[WebJars], versioned URLs like `/webjars/jquery/1.2.0/jquery.min.js` are the recommended and most efficient way to use them. The related resource location is configured out of the box with Spring Boot (or can be configured manually via `ResourceHandlerRegistry`) and does not require to add the -`org.webjars:webjars-locator-core` dependency. +`org.webjars:webjars-locator-lite` dependency. Version-less URLs like `/webjars/jquery/jquery.min.js` are supported through the `WebJarsResourceResolver` which is automatically registered when the -`org.webjars:webjars-locator-core` library is present on the classpath, at the cost of a -classpath scanning that could slow down application startup. The resolver can re-write URLs to -include the version of the jar and can also match against incoming URLs without versions +`org.webjars:webjars-locator-lite` library is present on the classpath. The resolver can re-write +URLs to include the version of the jar and can also match against incoming URLs without versions -- for example, from `/webjars/jquery/jquery.min.js` to `/webjars/jquery/1.2.0/jquery.min.js`. TIP: The Java configuration based on `ResourceHandlerRegistry` provides further options 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 403db0bbf2a8..f4af7ac1b4e4 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 e13037ded80d..3a0f94e57203 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/arguments.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc index 4e3b30ea3c09..fb4e6c5ed8d1 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/arguments.adoc @@ -30,8 +30,7 @@ and others) and is equivalent to `required=false`. | `jakarta.servlet.http.PushBuilder` | Servlet 4.0 push builder API for programmatic HTTP/2 resource pushes. - Note that, per the Servlet specification, the injected `PushBuilder` instance can be null if the client - does not support that HTTP/2 feature. + Note that this API has been deprecated as of Servlet 6.1. | `java.security.Principal` | Currently authenticated user -- possibly a specific `Principal` implementation class if known. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc index 440bc376ed84..247062157172 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-controller/ann-methods/matrix-variables.adoc @@ -157,9 +157,7 @@ Kotlin:: ---- ====== -Note that you need to enable the use of matrix variables. In the MVC Java configuration, -you need to set a `UrlPathHelper` with `removeSemicolonContent=false` through -xref:web/webmvc/mvc-config/path-matching.adoc[Path Matching]. In the MVC XML namespace, you can set +Note that you need to enable the use of matrix variables. In the MVC XML namespace, you can set ``. 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 7a04b5ba7f13..d9808acc0540 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 557de2db3ca2..bd6aa862ec1a 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 a8cc9756dec7..cfd4cbb2814e 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 @@ -248,36 +248,6 @@ specific than other pattern that do not have double wildcards. For the full details, follow the above links to the pattern Comparators. -[[mvc-ann-requestmapping-suffix-pattern-match]] -== Suffix Match - -Starting in 5.3, by default Spring MVC no longer performs `.{asterisk}` suffix pattern -matching where a controller mapped to `/person` is also implicitly mapped to -`/person.{asterisk}`. As a consequence path extensions are no longer used to interpret -the requested content type for the response -- for example, `/person.pdf`, `/person.xml`, -and so on. - -Using file extensions in this way was necessary when browsers used to send `Accept` headers -that were hard to interpret consistently. At present, that is no longer a necessity and -using the `Accept` header should be the preferred choice. - -Over time, the use of file name extensions has proven problematic in a variety of ways. -It can cause ambiguity when overlain with the use of URI variables, path parameters, and -URI encoding. Reasoning about URL-based authorization -and security (see next section for more details) also becomes more difficult. - -To completely disable the use of path extensions in versions prior to 5.3, set the following: - -* `useSuffixPatternMatching(false)`, see xref:web/webmvc/mvc-config/path-matching.adoc[PathMatchConfigurer] -* `favorPathExtension(false)`, see xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer] - -Having a way to request content types other than through the `"Accept"` header can still -be useful, for example, when typing a URL in a browser. A safe alternative to path extensions is -to use the query parameter strategy. If you must use file extensions, consider restricting -them to a list of explicitly registered extensions through the `mediaTypes` property of -xref:web/webmvc/mvc-config/content-negotiation.adoc[ContentNegotiationConfigurer]. - - [[mvc-ann-requestmapping-rfd]] == Suffix Match and RFD @@ -459,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 dd11e2edd769..99ddf8635eb1 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 @@ -57,7 +57,7 @@ locale and language specific resource bundles. For further custom handling of method validation errors, you can extend `ResponseEntityExceptionHandler` or use an `@ExceptionHandler` method in a controller or in a `@ControllerAdvice`, and handle `HandlerMethodValidationException` directly. -The exception contains a list of``ParameterValidationResult``s that group validation errors +The exception contains a list of ``ParameterValidationResult``s that group validation errors by method parameter. You can either iterate over those, or provide a visitor with callback methods by controller method parameter type: @@ -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/webmvc/mvc-http2.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc index da03fba9b4a1..e5e19ea705a0 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-http2.adoc @@ -4,12 +4,8 @@ [.small]#xref:web/webflux/http2.adoc[See equivalent in the Reactive stack]# -Servlet 4 containers are required to support HTTP/2, and Spring Framework 5 is compatible -with Servlet API 4. From a programming model perspective, there is nothing specific that +Servlet 4 containers are required to support HTTP/2, and Spring Framework requires +Servlet API 6.1. From a programming model perspective, there is nothing specific that applications need to do. However, there are considerations related to server configuration. For more details, see the {spring-framework-wiki}/HTTP-2-support[HTTP/2 wiki page]. - -The Servlet API does expose one construct related to HTTP/2. You can use the -`jakarta.servlet.http.PushBuilder` to proactively push resources to clients, and it -is supported as a xref:web/webmvc/mvc-controller/ann-methods/arguments.adoc[method argument] to `@RequestMapping` methods. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc index 446ae42c0414..9a4f769aa5a2 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-security.adoc @@ -13,7 +13,7 @@ reference documentation, including: * {docs-spring-security}/features/exploits/csrf.html#csrf-protection[CSRF protection] * {docs-spring-security}/features/exploits/headers.html[Security Response Headers] -https://hdiv.org/[HDIV] is another web security framework that integrates with Spring MVC. +https://github.com/hdiv/hdiv[HDIV] is another web security framework that integrates with Spring MVC. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/sequence.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/sequence.adoc index 427a4d0ec139..c0ceb61fd57f 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/sequence.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/sequence.adoc @@ -11,8 +11,6 @@ The `DispatcherServlet` processes requests as follows: * The locale resolver is bound to the request to let elements in the process resolve the locale to use when processing the request (rendering the view, preparing data, and so on). If you do not need locale resolving, you do not need the locale resolver. -* The theme resolver is bound to the request to let elements such as views determine - which theme to use. If you do not use themes, you can ignore it. * If you specify a multipart file resolver, the request is inspected for multiparts. If multiparts are found, the request is wrapped in a `MultipartHttpServletRequest` for further processing by other elements in the process. See xref:web/webmvc/mvc-servlet/multipart.adoc[Multipart Resolver] for further diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/special-bean-types.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/special-bean-types.adoc index edb52264bead..94148874fcd0 100644 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/special-bean-types.adoc +++ b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/special-bean-types.adoc @@ -43,10 +43,6 @@ The following table lists the special beans detected by the `DispatcherServlet`: | Resolve the `Locale` a client is using and possibly their time zone, in order to be able to offer internationalized views. See xref:web/webmvc/mvc-servlet/localeresolver.adoc[Locale]. -| xref:web/webmvc/mvc-servlet/themeresolver.adoc[`ThemeResolver`] -| Resolve themes your web application can use -- for example, to offer personalized layouts. - See xref:web/webmvc/mvc-servlet/themeresolver.adoc[Themes]. - | xref:web/webmvc/mvc-servlet/multipart.adoc[`MultipartResolver`] | Abstraction for parsing a multi-part request (for example, browser form file upload) with the help of some multipart parsing library. See xref:web/webmvc/mvc-servlet/multipart.adoc[Multipart Resolver]. diff --git a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/themeresolver.adoc b/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/themeresolver.adoc deleted file mode 100644 index fc4bc9a10301..000000000000 --- a/framework-docs/modules/ROOT/pages/web/webmvc/mvc-servlet/themeresolver.adoc +++ /dev/null @@ -1,92 +0,0 @@ -[[mvc-themeresolver]] -= Themes - -You can apply Spring Web MVC framework themes to set the overall look-and-feel of your -application, thereby enhancing user experience. A theme is a collection of static -resources, typically style sheets and images, that affect the visual style of the -application. - -WARNING: as of 6.0 support for themes has been deprecated theme in favor of using CSS, -and without any special support on the server side. - - -[[mvc-themeresolver-defining]] -== Defining a theme - -To use themes in your web application, you must set up an implementation of the -`org.springframework.ui.context.ThemeSource` interface. The `WebApplicationContext` -interface extends `ThemeSource` but delegates its responsibilities to a dedicated -implementation. By default, the delegate is an -`org.springframework.ui.context.support.ResourceBundleThemeSource` implementation that -loads properties files from the root of the classpath. To use a custom `ThemeSource` -implementation or to configure the base name prefix of the `ResourceBundleThemeSource`, -you can register a bean in the application context with the reserved name, `themeSource`. -The web application context automatically detects a bean with that name and uses it. - -When you use the `ResourceBundleThemeSource`, a theme is defined in a simple properties -file. The properties file lists the resources that make up the theme, as the following example shows: - -[literal,subs="verbatim,quotes"] ----- -styleSheet=/themes/cool/style.css -background=/themes/cool/img/coolBg.jpg ----- - -The keys of the properties are the names that refer to the themed elements from view -code. For a JSP, you typically do this using the `spring:theme` custom tag, which is -very similar to the `spring:message` tag. The following JSP fragment uses the theme -defined in the previous example to customize the look and feel: - -[source,xml,indent=0,subs="verbatim,quotes"] ----- - <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> - - - - - - ... - - ----- - -By default, the `ResourceBundleThemeSource` uses an empty base name prefix. As a result, -the properties files are loaded from the root of the classpath. Thus, you would put the -`cool.properties` theme definition in a directory at the root of the classpath (for -example, in `/WEB-INF/classes`). The `ResourceBundleThemeSource` uses the standard Java -resource bundle loading mechanism, allowing for full internationalization of themes. For -example, we could have a `/WEB-INF/classes/cool_nl.properties` that references a special -background image with Dutch text on it. - - -[[mvc-themeresolver-resolving]] -== Resolving Themes - -After you define themes, as described in the xref:web/webmvc/mvc-servlet/themeresolver.adoc#mvc-themeresolver-defining[preceding section], -you decide which theme to use. The `DispatcherServlet` looks for a bean named `themeResolver` -to find out which `ThemeResolver` implementation to use. A theme resolver works in much the same -way as a `LocaleResolver`. It detects the theme to use for a particular request and can also -alter the request's theme. The following table describes the theme resolvers provided by Spring: - -[[mvc-theme-resolver-impls-tbl]] -.ThemeResolver implementations -[cols="1,4"] -|=== -| Class | Description - -| `FixedThemeResolver` -| Selects a fixed theme, set by using the `defaultThemeName` property. - -| `SessionThemeResolver` -| The theme is maintained in the user's HTTP session. It needs to be set only once for - each session but is not persisted between sessions. - -| `CookieThemeResolver` -| The selected theme is stored in a cookie on the client. -|=== - -Spring also provides a `ThemeChangeInterceptor` that lets theme changes on every -request with a simple request parameter. - - - diff --git a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc index 13c005fedf34..1e2fcd1595eb 100644 --- a/framework-docs/modules/ROOT/pages/web/websocket/server.adoc +++ b/framework-docs/modules/ROOT/pages/web/websocket/server.adoc @@ -85,10 +85,7 @@ for all HTTP processing -- including WebSocket handshake and all other HTTP requests -- such as Spring MVC's `DispatcherServlet`. This is a significant limitation of JSR-356 that Spring's WebSocket support addresses with -server-specific `RequestUpgradeStrategy` implementations even when running in a JSR-356 runtime. -Such strategies currently exist for Tomcat, Jetty, GlassFish, WebLogic, WebSphere, and Undertow -(and WildFly). As of Jakarta WebSocket 2.1, a standard request upgrade strategy is available -which Spring chooses on Jakarta EE 10 based web containers such as Tomcat 10.1 and Jetty 12. +a standard `RequestUpgradeStrategy` implementation when running in a WebSocket API 2.1+ runtime. A secondary consideration is that Servlet containers with JSR-356 support are expected to perform a `ServletContainerInitializer` (SCI) scan that can slow down application 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 4301ba970868..831b1ff8dfae 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 a56c0d1893ed..fa1de23973a4 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/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java index 5d666cc9283b..44bb21e30cd9 100644 --- a/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java +++ b/framework-docs/src/main/java/org/springframework/docs/core/aot/hints/testing/SampleReflectionRuntimeHintsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,6 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; import org.springframework.aot.test.agent.RuntimeHintsInvocations; -import org.springframework.aot.test.agent.RuntimeHintsRecorder; import org.springframework.core.SpringVersion; import static org.assertj.core.api.Assertions.assertThat; @@ -33,6 +32,7 @@ // method is only enabled if the RuntimeHintsAgent is loaded on the current JVM. // It also tags tests with the "RuntimeHints" JUnit tag. @EnabledIfRuntimeHintsAgent +@SuppressWarnings("removal") class SampleReflectionRuntimeHintsTests { @Test @@ -43,7 +43,7 @@ void shouldRegisterReflectionHints() { typeHint.withMethod("getVersion", List.of(), ExecutableMode.INVOKE)); // Invoke the relevant piece of code we want to test within a recording lambda - RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + RuntimeHintsInvocations invocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> { SampleReflection sample = new SampleReflection(); sample.performReflection(); }); 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 63326e903aa4..7555aebc8e49 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.web.servlet.assertj.MockMvcTester; import org.springframework.web.context.WebApplicationContext; +@SuppressWarnings("removal") // tag::snippet[] @SpringJUnitWebConfig(ApplicationWebConfiguration.class) class AccountControllerIntegrationTests { diff --git a/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java similarity index 55% rename from spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java rename to framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java index 8ebc5397fad9..a69e95e856a5 100644 --- a/spring-web/src/test/java/org/springframework/http/client/OkHttp3ClientHttpRequestFactoryTests.java +++ b/framework-docs/src/main/java/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.java @@ -14,28 +14,19 @@ * limitations under the License. */ -package org.springframework.http.client; +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion; -import org.junit.jupiter.api.Test; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; -import org.springframework.http.HttpMethod; - -/** - * @author Roy Clarkson - */ -class OkHttp3ClientHttpRequestFactoryTests extends AbstractHttpRequestFactoryTests { - - @SuppressWarnings("removal") - @Override - protected ClientHttpRequestFactory createRequestFactory() { - return new OkHttp3ClientHttpRequestFactory(); - } +// tag::snippet[] +@Configuration +public class WebConfiguration implements WebMvcConfigurer { @Override - @Test - void httpMethods() throws Exception { - super.httpMethods(); - assertHttpMethod("patch", HttpMethod.PATCH); + 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 019a99a270cb..3517b3275e27 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.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 0d949748a323..1c1122713e9e 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.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 c4d27f555ebd..57592d56141a 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.WebMvcConfigurer; import org.springframework.web.servlet.view.json.MappingJackson2JsonView; +@SuppressWarnings("removal") // tag::snippet[] @Configuration public class WebConfiguration implements WebMvcConfigurer { diff --git a/spring-context/src/test/java/example/indexed/IndexedJakartaManagedBeanComponent.java b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.kt similarity index 74% rename from spring-context/src/test/java/example/indexed/IndexedJakartaManagedBeanComponent.java rename to framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.kt index ed640a7a73da..322698b06fa1 100644 --- a/spring-context/src/test/java/example/indexed/IndexedJakartaManagedBeanComponent.java +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/AnotherBean.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,7 @@ * limitations under the License. */ -package example.indexed; +package org.springframework.docs.core.beans.dependencies.beansfactorylazyinit -/** - * @author Sam Brannen - */ -@jakarta.annotation.ManagedBean -public class IndexedJakartaManagedBeanComponent { -} +class AnotherBean { +} \ No newline at end of file diff --git a/spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.kt similarity index 73% rename from spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java rename to framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.kt index 581be8a6f97d..b70d3a922512 100644 --- a/spring-context/src/test/java/example/indexed/IndexedJavaxNamedComponent.java +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/beans/dependencies/beansfactorylazyinit/ExpensiveToCreateBean.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,7 @@ * limitations under the License. */ -package example.indexed; +package org.springframework.docs.core.beans.dependencies.beansfactorylazyinit -/** - * @author Sam Brannen - */ -@javax.inject.Named("myIndexedJavaxNamedComponent") -public class IndexedJavaxNamedComponent { -} +class ExpensiveToCreateBean { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.kt new file mode 100644 index 000000000000..75c4d4a32179 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/CustomerPreferenceDao.kt @@ -0,0 +1,20 @@ +/* + * 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.core.expressions.expressionsbeandef + +class CustomerPreferenceDao { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.kt b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.kt new file mode 100644 index 000000000000..d49fa29a3969 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/core/expressions/expressionsbeandef/MovieFinder.kt @@ -0,0 +1,21 @@ +/* + * 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.core.expressions.expressionsbeandef + +class MovieFinder { +} \ No newline at end of file diff --git a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ResourceBundleViewResolverNoCacheTests.java b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.kt similarity index 73% rename from spring-webmvc/src/test/java/org/springframework/web/servlet/view/ResourceBundleViewResolverNoCacheTests.java rename to framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.kt index 3b8c14d3d9d6..ef93410bb2b1 100644 --- a/spring-webmvc/src/test/java/org/springframework/web/servlet/view/ResourceBundleViewResolverNoCacheTests.java +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItem.kt @@ -14,16 +14,8 @@ * limitations under the License. */ -package org.springframework.web.servlet.view; +package org.springframework.docs.dataaccess.jdbc.jdbccomplextypes -/** - * @author Rod Johnson - */ -class ResourceBundleViewResolverNoCacheTests extends ResourceBundleViewResolverTests { - - @Override - protected boolean getCache() { - return false; - } +import java.util.Date -} +data class TestItem(val id: Long, val description: String, val expirationDate: Date) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt index ac0c648ba6f7..5cdfb06bf389 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbccomplextypes/TestItemStoredProcedure.kt @@ -31,11 +31,11 @@ class TestItemStoredProcedure(dataSource: DataSource) : StoredProcedure(dataSour cs: CallableStatement, colIndx: Int, _: Int, _: String? -> val struct = cs.getObject(colIndx) as Struct val attr = struct.attributes - val item = TestItem() - item.id = (attr[0] as Number).toLong() - item.description = attr[1] as String - item.expirationDate = attr[2] as Date - item + TestItem( + (attr[0] as Number).toLong(), + attr[1] as String, + attr[2] as Date + ) }) // ... } diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.kt b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.kt new file mode 100644 index 000000000000..e73c56c373b4 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/dataaccess/jdbc/jdbcjdbctemplateidioms/CorporateEventDao.kt @@ -0,0 +1,20 @@ +/* + * 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.dataaccess.jdbc.jdbcjdbctemplateidioms + +interface CorporateEventDao { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt index f91e78a54875..0db14f210f45 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/cache/cachestoreconfigurationcaffeine/CustomCacheConfiguration.kt @@ -28,9 +28,7 @@ class CustomCacheConfiguration { // tag::snippet[] @Bean fun cacheManager(): CacheManager { - return CaffeineCacheManager().apply { - cacheNames = listOf("default", "books") - } + return CaffeineCacheManager("default", "books") } // end::snippet[] } \ No newline at end of file diff --git a/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.kt similarity index 71% rename from spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java rename to framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.kt index 6140ea0dce36..b82b4c1d9c74 100644 --- a/spring-context/src/test/java/example/scannable/JakartaManagedBeanComponent.java +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/IJmxTestBean.kt @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,11 +14,12 @@ * limitations under the License. */ -package example.scannable; +package org.springframework.docs.integration.jmx.jmxexporting -/** - * @author Sam Brannen - */ -@jakarta.annotation.ManagedBean("myJakartaManagedBeanComponent") -public class JakartaManagedBeanComponent { -} +interface IJmxTestBean { + + var name: String + var age: Int + fun add(x: Int, y: Int): Int + fun dontExposeMe() +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt index a4936ed4d947..07ae48ed5c69 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/jmx/jmxexporting/JmxTestBean.kt @@ -19,24 +19,8 @@ package org.springframework.docs.integration.jmx.jmxexporting // tag::snippet[] class JmxTestBean : IJmxTestBean { - private lateinit var name: String - private var age = 0 - - override fun getAge(): Int { - return age - } - - override fun setAge(age: Int) { - this.age = age - } - - override fun setName(name: String) { - this.name = name - } - - override fun getName(): String { - return name - } + override lateinit var name: String + override var age = 0 override fun add(x: Int, y: Int): Int { return x + y diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Customer.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Customer.kt new file mode 100644 index 000000000000..b42c891f766e --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Customer.kt @@ -0,0 +1,23 @@ +/* + * 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.integration.mailusagesimple + +data class Customer( + val emailAddress: String, + val firstName: String, + val lastName: String +) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Order.kt b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Order.kt new file mode 100644 index 000000000000..ba20e8eee1fa --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/integration/mailusagesimple/Order.kt @@ -0,0 +1,22 @@ +/* + * 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.integration.mailusagesimple + +data class Order( + val customer: Customer, + val orderNumber: String +) \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.kt new file mode 100644 index 000000000000..5328cfd2c04d --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/AccountController.kt @@ -0,0 +1,23 @@ +/* + * 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.testing.mockmvc.assertj.mockmvctestersetup + +import org.springframework.web.bind.annotation.RestController + +@RestController +class AccountController { +} \ No newline at end of file diff --git a/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.kt new file mode 100644 index 000000000000..97d474d2454f --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/testing/mockmvc/assertj/mockmvctestersetup/ApplicationWebConfiguration.kt @@ -0,0 +1,25 @@ +/* + * 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.testing.mockmvc.assertj.mockmvctestersetup + +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.EnableWebMvc + +@Configuration(proxyBeanMethods = false) +@EnableWebMvc +class ApplicationWebConfiguration { +} \ No newline at end of file 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 523d728941e4..196d6a63dbb4 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/spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryWithOkHttpTests.java b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt similarity index 55% rename from spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryWithOkHttpTests.java rename to framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt index 0d2436996ba7..474ff85a6d2d 100644 --- a/spring-web/src/test/java/org/springframework/http/client/BufferingClientHttpRequestFactoryWithOkHttpTests.java +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigapiversion/WebConfiguration.kt @@ -14,18 +14,18 @@ * limitations under the License. */ -package org.springframework.http.client; +package org.springframework.docs.web.webmvc.mvcconfig.mvcconfigapiversion -/** - * Tests for {@link BufferingClientHttpRequestWrapper} for clients - * not supporting non-null, empty request bodies for GET requests. - */ -class BufferingClientHttpRequestFactoryWithOkHttpTests extends AbstractHttpRequestFactoryTests { +import org.springframework.context.annotation.Configuration +import org.springframework.web.servlet.config.annotation.ApiVersionConfigurer +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer - @Override - @SuppressWarnings("removal") - protected ClientHttpRequestFactory createRequestFactory() { - return new BufferingClientHttpRequestFactory(new OkHttp3ClientHttpRequestFactory()); - } +// 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/mvcconfiginterceptors/WebConfiguration.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt index c2f6d8daba16..9bbb738f3abe 100644 --- a/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.kt @@ -14,14 +14,13 @@ * limitations under the License. */ -@file:Suppress("DEPRECATION") package org.springframework.docs.web.webmvc.mvcconfig.mvcconfiginterceptors import org.springframework.context.annotation.Configuration import org.springframework.web.servlet.config.annotation.InterceptorRegistry import org.springframework.web.servlet.config.annotation.WebMvcConfigurer +import org.springframework.web.servlet.handler.UserRoleAuthorizationInterceptor import org.springframework.web.servlet.i18n.LocaleChangeInterceptor -import org.springframework.web.servlet.theme.ThemeChangeInterceptor // tag::snippet[] @Configuration @@ -29,7 +28,7 @@ class WebConfiguration : WebMvcConfigurer { override fun addInterceptors(registry: InterceptorRegistry) { registry.addInterceptor(LocaleChangeInterceptor()) - registry.addInterceptor(ThemeChangeInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") + registry.addInterceptor(UserRoleAuthorizationInterceptor()).addPathPatterns("/**").excludePathPatterns("/admin/**") } } // 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 12c197a46f51..c1993d1208c0 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/mvcconfigvalidation/FooValidator.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.kt new file mode 100644 index 000000000000..ed19b4f0a6a0 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/webmvc/mvcconfig/mvcconfigvalidation/FooValidator.kt @@ -0,0 +1,27 @@ +/* + * 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.mvcconfigvalidation + +import org.springframework.validation.Errors +import org.springframework.validation.Validator + +class FooValidator : Validator { + override fun supports(clazz: Class<*>) = false + + override fun validate(target: Any, errors: Errors) { + } +} \ No newline at end of file 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 55acaa63cb11..5e300f72229e 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 472ecf25bf30..1b60cb562b60 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-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.kt b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.kt new file mode 100644 index 000000000000..e859cbba1971 --- /dev/null +++ b/framework-docs/src/main/kotlin/org/springframework/docs/web/websocket/websocketserverruntimeconfiguration/MyEchoHandler.kt @@ -0,0 +1,22 @@ +/* + * 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.websocket.websocketserverruntimeconfiguration + +import org.springframework.web.socket.handler.AbstractWebSocketHandler + +class MyEchoHandler : AbstractWebSocketHandler() { +} \ No newline at end of file diff --git a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml index 2d0f1cae1ead..51f91158b468 100644 --- a/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml +++ b/framework-docs/src/main/resources/org/springframework/docs/web/webmvc/mvcconfig/mvcconfiginterceptors/WebConfiguration.xml @@ -14,7 +14,9 @@ - + + + diff --git a/framework-platform/framework-platform.gradle b/framework-platform/framework-platform.gradle index fe4829dd0376..677d834b5d89 100644 --- a/framework-platform/framework-platform.gradle +++ b/framework-platform/framework-platform.gradle @@ -7,21 +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.netty:netty5-bom:5.0.0.Alpha5")) - api(platform("io.projectreactor:reactor-bom:2024.0.4")) + api(platform("com.fasterxml.jackson:jackson-bom:2.18.4")) + api(platform("io.micrometer:micrometer-bom:1.15.0")) + api(platform("io.netty:netty-bom:4.2.2.Final")) + api(platform("io.projectreactor:reactor-bom:2025.0.0-SNAPSHOT")) api(platform("io.rsocket:rsocket-bom:1.1.5")) - api(platform("org.apache.groovy:groovy-bom:4.0.26")) - api(platform("org.apache.logging.log4j:log4j-bom:2.21.1")) + api(platform("org.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.0.17")) - api(platform("org.eclipse.jetty.ee10:jetty-ee10-bom:12.0.17")) - api(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1")) - api(platform("org.jetbrains.kotlinx:kotlinx-serialization-bom:1.6.3")) - api(platform("org.junit:junit-bom:5.12.0")) - api(platform("org.mockito:mockito-bom:5.16.0")) + 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") @@ -30,26 +30,25 @@ 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") api("com.oracle.database.jdbc:ojdbc11:21.9.0.0") api("com.rometools:rome:1.19.0") api("com.squareup.okhttp3:mockwebserver:3.14.9") - api("com.squareup.okhttp3:okhttp:3.14.9") api("com.sun.activation:jakarta.activation:2.0.1") - api("com.sun.mail:jakarta.mail:2.0.1") api("com.sun.xml.bind:jaxb-core:3.0.2") api("com.sun.xml.bind:jaxb-impl:3.0.2") api("com.sun.xml.bind:jaxb-xjc:3.0.2") api("com.thoughtworks.qdox:qdox:2.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.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.netty:reactor-netty5-http:2.0.0-M3") api("io.projectreactor.tools:blockhound:1.0.8.RELEASE") api("io.r2dbc:r2dbc-h2:1.0.0.RELEASE") api("io.r2dbc:r2dbc-spi-test:1.0.0.RELEASE") @@ -60,32 +59,31 @@ dependencies { api("io.undertow:undertow-servlet:2.3.18.Final") api("io.undertow:undertow-websockets-jsr:2.3.18.Final") api("io.vavr:vavr:0.10.4") - api("jakarta.activation:jakarta.activation-api:2.0.1") - api("jakarta.annotation:jakarta.annotation-api:2.0.0") + api("jakarta.activation:jakarta.activation-api:2.1.3") + api("jakarta.annotation:jakarta.annotation-api:3.0.0") + api("jakarta.validation:jakarta.validation-api:3.1.0") api("jakarta.ejb:jakarta.ejb-api:4.0.1") - api("jakarta.el:jakarta.el-api:4.0.0") - api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:2.0.0") - api("jakarta.faces:jakarta.faces-api:3.0.0") + api("jakarta.el:jakarta.el-api:6.0.1") + api("jakarta.enterprise.concurrent:jakarta.enterprise.concurrent-api:3.1.1") + api("jakarta.faces:jakarta.faces-api:4.1.2") api("jakarta.inject:jakarta.inject-api:2.0.1") api("jakarta.inject:jakarta.inject-tck:2.0.1") - api("jakarta.interceptor:jakarta.interceptor-api:2.0.0") - api("jakarta.jms:jakarta.jms-api:3.0.0") - api("jakarta.json.bind:jakarta.json.bind-api:2.0.0") - api("jakarta.json:jakarta.json-api:2.0.1") - api("jakarta.mail:jakarta.mail-api:2.0.1") - api("jakarta.persistence:jakarta.persistence-api:3.0.0") - api("jakarta.resource:jakarta.resource-api:2.0.0") - api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.0") - api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:3.1.1") - api("jakarta.servlet:jakarta.servlet-api:6.0.0") + api("jakarta.interceptor:jakarta.interceptor-api:2.2.0") + api("jakarta.jms:jakarta.jms-api:3.1.0") + api("jakarta.json.bind:jakarta.json.bind-api:3.0.1") + api("jakarta.json:jakarta.json-api:2.1.3") + api("jakarta.mail:jakarta.mail-api:2.1.3") + api("jakarta.persistence:jakarta.persistence-api:3.2.0") + api("jakarta.resource:jakarta.resource-api:2.1.0") + api("jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api:3.0.2") + api("jakarta.servlet.jsp:jakarta.servlet.jsp-api:4.0.0") + api("jakarta.servlet:jakarta.servlet-api:6.1.0") api("jakarta.transaction:jakarta.transaction-api:2.0.1") - api("jakarta.validation:jakarta.validation-api:3.0.2") - api("jakarta.websocket:jakarta.websocket-api:2.1.0") - api("jakarta.websocket:jakarta.websocket-client-api:2.1.0") + api("jakarta.validation:jakarta.validation-api:3.1.0") + api("jakarta.websocket:jakarta.websocket-api:2.2.0") + api("jakarta.websocket:jakarta.websocket-client-api:2.2.0") api("jakarta.xml.bind:jakarta.xml.bind-api:3.0.1") - api("javax.annotation:javax.annotation-api:1.3.2") api("javax.cache:cache-api:1.1.1") - api("javax.inject:javax.inject:1") api("javax.money:money-api:1.1") api("jaxen:jaxen:1.2.0") api("junit:junit:4.13.2") @@ -100,51 +98,52 @@ 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:10.1.28") - api("org.apache.tomcat.embed:tomcat-embed-websocket:10.1.28") - api("org.apache.tomcat:tomcat-util:10.1.28") - api("org.apache.tomcat:tomcat-websocket:10.1.28") - api("org.aspectj:aspectjrt:1.9.22.1") - api("org.aspectj:aspectjtools:1.9.22.1") - api("org.aspectj:aspectjweaver:1.9.22.1") + 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") api("org.crac:crac:1.4.0") api("org.dom4j:dom4j:2.1.4") api("org.easymock:easymock:5.5.0") + api("org.eclipse.angus:angus-mail:2.0.3") api("org.eclipse.jetty:jetty-reactive-httpclient:4.0.9") - api("org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.4") - api("org.eclipse:yasson:2.0.4") + api("org.eclipse.persistence:org.eclipse.persistence.jpa:5.0.0-B04") + api("org.eclipse:yasson:3.0.4") api("org.ehcache:ehcache:3.10.8") api("org.ehcache:jcache:1.0.1") api("org.freemarker:freemarker:2.3.34") api("org.glassfish.external:opendmk_jmxremote_optional_jar:1.0-b01-ea") api("org.glassfish:jakarta.el:4.0.2") - api("org.glassfish.tyrus:tyrus-container-servlet:2.1.3") api("org.graalvm.sdk:graal-sdk:22.3.1") - api("org.hamcrest:hamcrest:2.2") - api("org.hibernate:hibernate-core-jakarta:5.6.15.Final") - api("org.hibernate:hibernate-validator:7.0.5.Final") + api("org.hamcrest:hamcrest:3.0") + 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") api("org.mozilla:rhino:1.7.15") api("org.ogce:xpp3:1.1.6") api("org.python:jython-standalone:2.7.4") api("org.quartz-scheduler:quartz:2.3.2") + api("org.reactivestreams:reactive-streams:1.0.4") api("org.seleniumhq.selenium:htmlunit3-driver:4.29.0") api("org.seleniumhq.selenium:selenium-java:4.29.0") - api("org.skyscreamer:jsonassert:1.5.3") - api("org.slf4j:slf4j-api:2.0.17") + api("org.skyscreamer:jsonassert:2.0-rc1") api("org.testng:testng:7.11.0") api("org.webjars:underscorejs:1.8.3") - api("org.webjars:webjars-locator-core:0.59") api("org.webjars:webjars-locator-lite:1.1.0") api("org.xmlunit:xmlunit-assertj:2.10.0") api("org.xmlunit:xmlunit-matchers:2.10.0") diff --git a/gradle.properties b/gradle.properties index b593061368c3..c9cccd858af0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,10 +1,10 @@ -version=6.2.4-SNAPSHOT +version=7.0.0-SNAPSHOT org.gradle.caching=true org.gradle.jvmargs=-Xmx2048m org.gradle.parallel=true -kotlinVersion=1.9.25 +kotlinVersion=2.2.0-RC kotlin.jvm.target.validation.mode=ignore kotlin.stdlib.default.dependency=false diff --git a/gradle/ide.gradle b/gradle/ide.gradle index fbfa2c804b95..56865ee11b8b 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 @@ -98,7 +98,7 @@ if (project.name == "spring-oxm") { } // Include project specific settings -task eclipseSettings(type: Copy) { +tasks.register('eclipseSettings', Copy) { from rootProject.files( 'src/eclipse/org.eclipse.core.resources.prefs', 'src/eclipse/org.eclipse.jdt.core.prefs', @@ -107,7 +107,7 @@ task eclipseSettings(type: Copy) { outputs.upToDateWhen { false } } -task cleanEclipseSettings(type: Delete) { +tasks.register('cleanEclipseSettings', Delete) { delete project.file('.settings/org.eclipse.core.resources.prefs') delete project.file('.settings/org.eclipse.jdt.core.prefs') delete project.file('.settings/org.eclipse.jdt.ui.prefs') diff --git a/gradle/spring-module.gradle b/gradle/spring-module.gradle index ff48a66e39e0..076a67b192f3 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.10.26' - errorprone 'com.google.errorprone:error_prone_core:2.9.0' } pluginManager.withPlugin("kotlin") { @@ -69,32 +67,45 @@ 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) - // Check for syntax during linting. 'none' doesn't seem to work in suppressing - // all linting warnings all the time (see/link references most notably). - options.addStringOption("Xdoclint:syntax", "-quiet") - - // 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 } -task sourcesJar(type: Jar, dependsOn: classes) { +tasks.register('sourcesJar', Jar) { + dependsOn classes duplicatesStrategy = DuplicatesStrategy.EXCLUDE archiveClassifier.set("sources") from sourceSets.main.allSource // Don't include or exclude anything explicitly by default. See SPR-12085. } -task javadocJar(type: Jar) { +tasks.register('javadocJar', Jar) { archiveClassifier.set("javadoc") from javadoc } @@ -113,17 +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:CustomContractAnnotations", "org.springframework.lang.Contract") - option("NullAway:AnnotatedPackages", "org.springframework") - option("NullAway:UnannotatedSubPackages", "org.springframework.instrument,org.springframework.context.index," + - "org.springframework.asm,org.springframework.cglib,org.springframework.objenesis," + - "org.springframework.javapoet,org.springframework.aot.nativex.substitution,org.springframework.aot.nativex.feature") - } -} -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/toolchains.gradle b/gradle/toolchains.gradle deleted file mode 100644 index 8c5d248136da..000000000000 --- a/gradle/toolchains.gradle +++ /dev/null @@ -1,99 +0,0 @@ -/** - * Apply the JVM Toolchain conventions - * See https://docs.gradle.org/current/userguide/toolchains.html - * - * One can choose the toolchain to use for compiling and running the TEST sources. - * These options apply to Java, Kotlin and Groovy test sources when available. - * {@code "./gradlew check -PtestToolchain=22"} will use a JDK22 - * toolchain for compiling and running the test SourceSet. - * - * By default, the main build will fall back to using the a JDK 17 - * toolchain (and 17 language level) for all main sourceSets. - * See {@link org.springframework.build.JavaConventions}. - * - * Gradle will automatically detect JDK distributions in well-known locations. - * The following command will list the detected JDKs on the host. - * {@code - * $ ./gradlew -q javaToolchains - * } - * - * We can also configure ENV variables and let Gradle know about them: - * {@code - * $ echo JDK17 - * /opt/openjdk/java17 - * $ echo JDK22 - * /opt/openjdk/java22 - * $ ./gradlew -Porg.gradle.java.installations.fromEnv=JDK17,JDK22 check - * } - * - * @author Brian Clozel - * @author Sam Brannen - */ - -def testToolchainConfigured() { - return project.hasProperty('testToolchain') && project.testToolchain -} - -def testToolchainLanguageVersion() { - if (testToolchainConfigured()) { - return JavaLanguageVersion.of(project.testToolchain.toString()) - } - return JavaLanguageVersion.of(17) -} - -plugins.withType(JavaPlugin).configureEach { - // Configure a specific Java Toolchain for compiling and running tests if the 'testToolchain' property is defined - if (testToolchainConfigured()) { - def testLanguageVersion = testToolchainLanguageVersion() - tasks.withType(JavaCompile).matching { it.name.contains("Test") }.configureEach { - javaCompiler = javaToolchains.compilerFor { - languageVersion = testLanguageVersion - } - } - tasks.withType(Test).configureEach{ - javaLauncher = javaToolchains.launcherFor { - languageVersion = testLanguageVersion - } - // Enable Java experimental support in Bytebuddy - // Bytebuddy 1.15.4 supports JDK <= 24 - // see https://github.com/raphw/byte-buddy/blob/master/release-notes.md - if (testLanguageVersion.compareTo(JavaLanguageVersion.of(24)) > 0 ) { - jvmArgs("-Dnet.bytebuddy.experimental=true") - } - } - } -} - -// Configure the JMH plugin to use the toolchain for generating and running JMH bytecode -pluginManager.withPlugin("me.champeau.jmh") { - if (testToolchainConfigured()) { - tasks.matching { it.name.contains('jmh') && it.hasProperty('javaLauncher') }.configureEach { - javaLauncher.set(javaToolchains.launcherFor { - languageVersion.set(testToolchainLanguageVersion()) - }) - } - tasks.withType(JavaCompile).matching { it.name.contains("Jmh") }.configureEach { - javaCompiler = javaToolchains.compilerFor { - languageVersion = testToolchainLanguageVersion() - } - } - } -} - -// Store resolved Toolchain JVM information as custom values in the build scan. -rootProject.ext { - resolvedMainToolchain = false - resolvedTestToolchain = false -} -gradle.taskGraph.afterTask { Task task, TaskState state -> - if (!resolvedMainToolchain && task instanceof JavaCompile && task.javaCompiler.isPresent()) { - def metadata = task.javaCompiler.get().metadata - task.project.develocity.buildScan.value('Main toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedMainToolchain = true - } - if (testToolchainConfigured() && !resolvedTestToolchain && task instanceof Test && task.javaLauncher.isPresent()) { - def metadata = task.javaLauncher.get().metadata - task.project.develocity.buildScan.value('Test toolchain', "$metadata.vendor $metadata.languageVersion ($metadata.installationPath)") - resolvedTestToolchain = true - } -} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 9bbc975c742b..1b33c55baabb 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 37f853b1c84d..ff23a68d70f3 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.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 faf93008b77e..23d15a936707 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 9b42019c7915..5eed7ee84528 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/integration-tests.gradle b/integration-tests/integration-tests.gradle index 1444b2bb210b..b8b3fc13e34b 100644 --- a/integration-tests/integration-tests.gradle +++ b/integration-tests/integration-tests.gradle @@ -26,7 +26,7 @@ dependencies { testImplementation("jakarta.servlet:jakarta.servlet-api") testImplementation("org.aspectj:aspectjweaver") testImplementation("org.hsqldb:hsqldb") - testImplementation("org.hibernate:hibernate-core-jakarta") + testImplementation("org.hibernate.orm:hibernate-core") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") } 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 15fbd10d787a..b416fc3edf70 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 @@ -20,6 +20,7 @@ import java.util.List; import jakarta.servlet.ServletException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.support.AopUtils; @@ -31,7 +32,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.transaction.NoTransactionException; import org.springframework.transaction.interceptor.TransactionInterceptor; import org.springframework.transaction.testfixture.CallCountingTransactionManager; diff --git a/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java b/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java index 541025c19c17..1a1123c22670 100644 --- a/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java +++ b/integration-tests/src/test/java/org/springframework/aot/test/ReflectionInvocationsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,24 +18,23 @@ import org.junit.jupiter.api.Test; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; import org.springframework.aot.test.agent.RuntimeHintsInvocations; -import org.springframework.aot.test.agent.RuntimeHintsRecorder; import static org.assertj.core.api.Assertions.assertThat; @EnabledIfRuntimeHintsAgent +@SuppressWarnings("removal") class ReflectionInvocationsTests { @Test void sampleTest() { RuntimeHints hints = new RuntimeHints(); - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + hints.reflection().registerType(String.class); - RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + RuntimeHintsInvocations invocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> { SampleReflection sample = new SampleReflection(); sample.sample(); // does Method[] methods = String.class.getMethods(); }); @@ -45,9 +44,9 @@ void sampleTest() { @Test void multipleCallsTest() { RuntimeHints hints = new RuntimeHints(); - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - hints.reflection().registerType(Integer.class,MemberCategory.INTROSPECT_PUBLIC_METHODS); - RuntimeHintsInvocations invocations = RuntimeHintsRecorder.record(() -> { + hints.reflection().registerType(String.class); + hints.reflection().registerType(Integer.class); + RuntimeHintsInvocations invocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> { SampleReflection sample = new SampleReflection(); sample.multipleCalls(); // does Method[] methods = String.class.getMethods(); methods = Integer.class.getMethods(); }); 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 248000ce7bc7..f482ffd50f2e 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 @@ 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 20be17f8e087..496091ee58bb 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,7 +1,5 @@ plugins { - id "com.gradle.develocity" version "3.19" - id "io.spring.ge.conventions" version "0.0.17" - id "org.gradle.toolchains.foojay-resolver-convention" version "0.7.0" + id "io.spring.develocity.conventions" version "0.0.22" } include "spring-aop" @@ -14,7 +12,6 @@ include "spring-core" include "spring-core-test" include "spring-expression" include "spring-instrument" -include "spring-jcl" include "spring-jdbc" include "spring-jms" include "spring-messaging" diff --git a/spring-aop/spring-aop.gradle b/spring-aop/spring-aop.gradle index 2e166980450d..eec30b7bedff 100644 --- a/spring-aop/spring-aop.gradle +++ b/spring-aop/spring-aop.gradle @@ -5,12 +5,12 @@ apply plugin: "kotlin" dependencies { api(project(":spring-beans")) api(project(":spring-core")) + compileOnly("com.google.code.findbugs:jsr305") // for the AOP Alliance fork optional("org.apache.commons:commons-pool2") optional("org.aspectj:aspectjweaver") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") testFixturesImplementation(testFixtures(project(":spring-beans"))) testFixturesImplementation(testFixtures(project(":spring-core"))) - testFixturesImplementation("com.google.code.findbugs:jsr305") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-core"))) diff --git a/spring-aop/src/main/java/org/aopalliance/aop/package-info.java b/spring-aop/src/main/java/org/aopalliance/aop/package-info.java index add1d414f6d7..13e41680fcc4 100644 --- a/spring-aop/src/main/java/org/aopalliance/aop/package-info.java +++ b/spring-aop/src/main/java/org/aopalliance/aop/package-info.java @@ -1,4 +1,7 @@ /** * The core AOP Alliance advice marker. */ +@NullMarked package org.aopalliance.aop; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java index a814d1f8a7e8..5d3e1e360500 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInterceptor.java @@ -16,8 +16,6 @@ package org.aopalliance.intercept; -import javax.annotation.Nonnull; - /** * Intercepts the construction of a new object. * @@ -56,7 +54,6 @@ public interface ConstructorInterceptor extends Interceptor { * @throws Throwable if the interceptors or the target object * throws an exception */ - @Nonnull Object construct(ConstructorInvocation invocation) throws Throwable; } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java index 72951383e959..867925b06582 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/ConstructorInvocation.java @@ -18,8 +18,6 @@ import java.lang.reflect.Constructor; -import javax.annotation.Nonnull; - /** * Description of an invocation to a constructor, given to an * interceptor upon constructor-call. @@ -38,7 +36,6 @@ public interface ConstructorInvocation extends Invocation { * {@link Joinpoint#getStaticPart()} method (same result). * @return the constructor being called */ - @Nonnull Constructor getConstructor(); } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java index 96caaefefe00..82010f20ac56 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Invocation.java @@ -16,7 +16,7 @@ package org.aopalliance.intercept; -import javax.annotation.Nonnull; +import org.jspecify.annotations.Nullable; /** * This interface represents an invocation in the program. @@ -34,7 +34,6 @@ public interface Invocation extends Joinpoint { * array to change the arguments. * @return the argument of the invocation */ - @Nonnull - Object[] getArguments(); + @Nullable Object[] getArguments(); } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java index b9755389409b..b0a62e5c9248 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/Joinpoint.java @@ -18,8 +18,7 @@ import java.lang.reflect.AccessibleObject; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * This interface represents a generic runtime joinpoint (in the AOP @@ -49,23 +48,20 @@ public interface Joinpoint { * @return see the children interfaces' proceed definition * @throws Throwable if the joinpoint throws an exception */ - @Nullable - Object proceed() throws Throwable; + @Nullable Object proceed() throws Throwable; /** * Return the object that holds the current joinpoint's static part. *

For instance, the target object for an invocation. * @return the object (can be null if the accessible object is static) */ - @Nullable - Object getThis(); + @Nullable Object getThis(); /** * Return the static part of this joinpoint. *

The static part is an accessible object on which a chain of * interceptors is installed. */ - @Nonnull AccessibleObject getStaticPart(); } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java index a601fba50c6d..b75f738d5662 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInterceptor.java @@ -16,8 +16,7 @@ package org.aopalliance.intercept; -import javax.annotation.Nonnull; -import javax.annotation.Nullable; +import org.jspecify.annotations.Nullable; /** * Intercepts calls on an interface on its way to the target. These @@ -55,7 +54,6 @@ public interface MethodInterceptor extends Interceptor { * @throws Throwable if the interceptors or the target object * throws an exception */ - @Nullable - Object invoke(@Nonnull MethodInvocation invocation) throws Throwable; + @Nullable Object invoke(MethodInvocation invocation) throws Throwable; } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java index f1f511bea4cb..3d73f3d12f04 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/MethodInvocation.java @@ -18,8 +18,6 @@ import java.lang.reflect.Method; -import javax.annotation.Nonnull; - /** * Description of an invocation to a method, given to an interceptor * upon method-call. @@ -38,7 +36,6 @@ public interface MethodInvocation extends Invocation { * {@link Joinpoint#getStaticPart()} method (same result). * @return the method being called */ - @Nonnull Method getMethod(); } diff --git a/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java b/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java index 11ada4f9467a..baa3204ad539 100644 --- a/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java +++ b/spring-aop/src/main/java/org/aopalliance/intercept/package-info.java @@ -1,4 +1,7 @@ /** * The AOP Alliance reflective interception abstraction. */ +@NullMarked package org.aopalliance.intercept; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/aopalliance/package-info.java b/spring-aop/src/main/java/org/aopalliance/package-info.java index a525a32aec87..ff3342de1980 100644 --- a/spring-aop/src/main/java/org/aopalliance/package-info.java +++ b/spring-aop/src/main/java/org/aopalliance/package-info.java @@ -1,4 +1,7 @@ /** * Spring's variant of the AOP Alliance interfaces. */ +@NullMarked package org.aopalliance; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java b/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java index 8c2c5d6ef8f0..55cc71ee41b3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/AfterReturningAdvice.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * After returning advice is invoked only on normal method return, not if an @@ -41,6 +41,6 @@ public interface AfterReturningAdvice extends AfterAdvice { * allowed by the method signature. Otherwise the exception * will be wrapped as a runtime exception. */ - void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable; + void afterReturning(@Nullable Object returnValue, Method method, @Nullable Object[] args, @Nullable Object target) throws Throwable; } diff --git a/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java index 806744d09c31..84963941f6a3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/MethodBeforeAdvice.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. @@ -18,7 +18,7 @@ import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Advice invoked before a method is invoked. Such advices cannot @@ -40,6 +40,6 @@ public interface MethodBeforeAdvice extends BeforeAdvice { * allowed by the method signature. Otherwise the exception * will be wrapped as a runtime exception. */ - void before(Method method, Object[] args, @Nullable Object target) throws Throwable; + void before(Method method, @Nullable Object[] args, @Nullable Object target) throws Throwable; } diff --git a/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java index 9e04831a0150..f1b175fbcce1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/MethodMatcher.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + /** * Part of a {@link Pointcut}: Checks whether the target method is eligible for advice. * @@ -94,7 +96,7 @@ public interface MethodMatcher { * @return whether there's a runtime match * @see #matches(Method, Class) */ - boolean matches(Method method, Class targetClass, Object... args); + boolean matches(Method method, Class targetClass, @Nullable Object... args); /** diff --git a/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java b/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java index 2cc637621c90..a85fba986e15 100644 --- a/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java +++ b/spring-aop/src/main/java/org/springframework/aop/ProxyMethodInvocation.java @@ -17,8 +17,7 @@ package org.springframework.aop; import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Extension of the AOP Alliance {@link org.aopalliance.intercept.MethodInvocation} @@ -59,14 +58,14 @@ public interface ProxyMethodInvocation extends MethodInvocation { * @return an invocable clone of this invocation. * {@code proceed()} can be called once per clone. */ - MethodInvocation invocableClone(Object... arguments); + MethodInvocation invocableClone(@Nullable Object... arguments); /** * Set the arguments to be used on subsequent invocations in the any advice * in this chain. * @param arguments the argument array */ - void setArguments(Object... arguments); + void setArguments(@Nullable Object... arguments); /** * Add the specified user attribute with the given value to this invocation. @@ -83,7 +82,6 @@ public interface ProxyMethodInvocation extends MethodInvocation { * @return the value of the attribute, or {@code null} if not set * @see #setUserAttribute */ - @Nullable - Object getUserAttribute(String key); + @Nullable Object getUserAttribute(String key); } diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java b/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java index d518ddb444a0..df550f771160 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java +++ b/spring-aop/src/main/java/org/springframework/aop/TargetClassAware.java @@ -16,7 +16,7 @@ package org.springframework.aop; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Minimal interface for exposing the target class behind a proxy. @@ -36,7 +36,6 @@ public interface TargetClassAware { * (typically a proxy configuration or an actual proxy). * @return the target Class, or {@code null} if not known */ - @Nullable - Class getTargetClass(); + @Nullable Class getTargetClass(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java index c19982f31916..9429d43bb7bc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/TargetSource.java @@ -16,7 +16,7 @@ package org.springframework.aop; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@code TargetSource} is used to obtain the current "target" of @@ -42,8 +42,7 @@ public interface TargetSource extends TargetClassAware { * @return the type of targets returned by this {@link TargetSource} */ @Override - @Nullable - Class getTargetClass(); + @Nullable Class getTargetClass(); /** * Will all calls to {@link #getTarget()} return the same object? @@ -64,8 +63,7 @@ default boolean isStatic() { * or {@code null} if there is no actual target instance * @throws Exception if the target object can't be resolved */ - @Nullable - Object getTarget() throws Exception; + @Nullable Object getTarget() throws Exception; /** * Release the given target object obtained from the diff --git a/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java index 6498627d6fe2..022944624378 100644 --- a/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/TrueMethodMatcher.java @@ -19,6 +19,8 @@ import java.io.Serializable; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + /** * Canonical MethodMatcher instance that matches all methods. * @@ -48,7 +50,7 @@ public boolean matches(Method method, Class targetClass) { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { // Should never be invoked as isRuntime returns false. throw new UnsupportedOperationException(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java index f6278b109709..6e3ad2db57da 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AbstractAspectJAdvice.java @@ -31,6 +31,7 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.weaver.tools.JoinPointMatch; import org.aspectj.weaver.tools.PointcutParameter; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AopInvocationException; import org.springframework.aop.MethodMatcher; @@ -42,7 +43,7 @@ import org.springframework.aop.support.StaticMethodMatcher; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -118,16 +119,13 @@ public static JoinPoint currentJoinPoint() { * This will be non-null if the creator of this advice object knows the argument names * and sets them explicitly. */ - @Nullable - private String[] argumentNames; + private @Nullable String @Nullable [] argumentNames; /** Non-null if after throwing advice binds the thrown value. */ - @Nullable - private String throwingName; + private @Nullable String throwingName; /** Non-null if after returning advice binds the return value. */ - @Nullable - private String returningName; + private @Nullable String returningName; private Class discoveredReturningType = Object.class; @@ -145,13 +143,11 @@ public static JoinPoint currentJoinPoint() { */ private int joinPointStaticPartArgumentIndex = -1; - @Nullable - private Map argumentBindings; + private @Nullable Map argumentBindings; private boolean argumentsIntrospected = false; - @Nullable - private Type discoveredReturningGenericType; + private @Nullable Type discoveredReturningGenericType; // Note: Unlike return type, no such generic information is needed for the throwing type, // since Java doesn't allow exception types to be parameterized. @@ -212,8 +208,7 @@ public final AspectInstanceFactory getAspectInstanceFactory() { /** * Return the ClassLoader for aspect instances. */ - @Nullable - public final ClassLoader getAspectClassLoader() { + public final @Nullable ClassLoader getAspectClassLoader() { return this.aspectInstanceFactory.getAspectClassLoader(); } @@ -264,10 +259,11 @@ public void setArgumentNames(String argumentNames) { * or in an advice annotation. * @param argumentNames list of argument names */ - public void setArgumentNamesFromStringArray(String... argumentNames) { + public void setArgumentNamesFromStringArray(@Nullable String... argumentNames) { this.argumentNames = new String[argumentNames.length]; for (int i = 0; i < argumentNames.length; i++) { - this.argumentNames[i] = argumentNames[i].strip(); + String argumentName = argumentNames[i]; + this.argumentNames[i] = argumentName != null ? argumentName.strip() : null; if (!isVariableName(this.argumentNames[i])) { throw new IllegalArgumentException( "'argumentNames' property of AbstractAspectJAdvice contains an argument name '" + @@ -281,9 +277,9 @@ public void setArgumentNamesFromStringArray(String... argumentNames) { if (argType == JoinPoint.class || argType == ProceedingJoinPoint.class || argType == JoinPoint.StaticPart.class) { - String[] oldNames = this.argumentNames; - this.argumentNames = new String[oldNames.length + 1]; - System.arraycopy(oldNames, 0, this.argumentNames, 0, i); + @Nullable String[] oldNames = this.argumentNames; + this.argumentNames = new String[oldNames.length + 1]; + System.arraycopy(oldNames, 0, this.argumentNames, 0, i); this.argumentNames[i] = "THIS_JOIN_POINT"; System.arraycopy(oldNames, i, this.argumentNames, i + 1, oldNames.length - i); break; @@ -322,8 +318,7 @@ protected Class getDiscoveredReturningType() { return this.discoveredReturningType; } - @Nullable - protected Type getDiscoveredReturningGenericType() { + protected @Nullable Type getDiscoveredReturningGenericType() { return this.discoveredReturningGenericType; } @@ -357,7 +352,8 @@ protected Class getDiscoveredThrowingType() { return this.discoveredThrowingType; } - private static boolean isVariableName(String name) { + @Contract("null -> false") + private static boolean isVariableName(@Nullable String name) { return AspectJProxyUtils.isVariableName(name); } @@ -467,6 +463,7 @@ protected ParameterNameDiscoverer createParameterNameDiscoverer() { return discoverer; } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void bindExplicitArguments(int numArgumentsLeftToBind) { Assert.state(this.argumentNames != null, "No argument names available"); this.argumentBindings = new HashMap<>(); @@ -556,14 +553,13 @@ private void configurePointcutParameters(String[] argumentNames, int argumentInd * @param ex the exception thrown by the method execution (may be null) * @return the empty array if there are no arguments */ - @SuppressWarnings("NullAway") - protected Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch, + protected @Nullable Object[] argBinding(JoinPoint jp, @Nullable JoinPointMatch jpMatch, @Nullable Object returnValue, @Nullable Throwable ex) { calculateArgumentBindings(); // AMC start - Object[] adviceInvocationArgs = new Object[this.parameterTypes.length]; + @Nullable Object[] adviceInvocationArgs = new Object[this.parameterTypes.length]; int numBound = 0; if (this.joinPointArgumentIndex != -1) { @@ -582,6 +578,7 @@ else if (this.joinPointStaticPartArgumentIndex != -1) { for (PointcutParameter parameter : parameterBindings) { String name = parameter.getName(); Integer index = this.argumentBindings.get(name); + Assert.state(index != null, "Index must not be null"); adviceInvocationArgs[index] = parameter.getBinding(); numBound++; } @@ -589,12 +586,14 @@ else if (this.joinPointStaticPartArgumentIndex != -1) { // binding from returning clause if (this.returningName != null) { Integer index = this.argumentBindings.get(this.returningName); + Assert.state(index != null, "Index must not be null"); adviceInvocationArgs[index] = returnValue; numBound++; } // binding from thrown exception if (this.throwingName != null) { Integer index = this.argumentBindings.get(this.throwingName); + Assert.state(index != null, "Index must not be null"); adviceInvocationArgs[index] = ex; numBound++; } @@ -632,8 +631,8 @@ protected Object invokeAdviceMethod(JoinPoint jp, @Nullable JoinPointMatch jpMat return invokeAdviceMethodWithGivenArgs(argBinding(jp, jpMatch, returnValue, t)); } - protected Object invokeAdviceMethodWithGivenArgs(Object[] args) throws Throwable { - Object[] actualArgs = args; + protected Object invokeAdviceMethodWithGivenArgs(@Nullable Object[] args) throws Throwable { + @Nullable Object[] actualArgs = args; if (this.aspectJAdviceMethod.getParameterCount() == 0) { actualArgs = null; } @@ -661,8 +660,7 @@ protected JoinPoint getJoinPoint() { /** * Get the current join point match at the join point we are being dispatched on. */ - @Nullable - protected JoinPointMatch getJoinPointMatch() { + protected @Nullable JoinPointMatch getJoinPointMatch() { MethodInvocation mi = ExposeInvocationInterceptor.currentInvocation(); if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); @@ -676,8 +674,7 @@ protected JoinPointMatch getJoinPointMatch() { // 'last man wins' which is not what we want at all. // Using the expression is guaranteed to be safe, since 2 identical expressions // are guaranteed to bind in exactly the same way. - @Nullable - protected JoinPointMatch getJoinPointMatch(ProxyMethodInvocation pmi) { + protected @Nullable JoinPointMatch getJoinPointMatch(ProxyMethodInvocation pmi) { String expression = this.pointcut.getExpression(); return (expression != null ? (JoinPointMatch) pmi.getUserAttribute(expression) : null); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java index 4ddf6303edd5..b4367ad04aac 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectInstanceFactory.java @@ -16,8 +16,9 @@ package org.springframework.aop.aspectj; +import org.jspecify.annotations.Nullable; + import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; /** * Interface implemented to provide an instance of an AspectJ aspect. @@ -44,7 +45,6 @@ public interface AspectInstanceFactory extends Ordered { * @return the aspect class loader (or {@code null} for the bootstrap loader) * @see org.springframework.util.ClassUtils#getDefaultClassLoader() */ - @Nullable - ClassLoader getAspectClassLoader(); + @Nullable ClassLoader getAspectClassLoader(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java index ec9b634ff89f..b97886c61c61 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAdviceParameterNameDiscoverer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -28,9 +28,9 @@ import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.weaver.tools.PointcutParser; import org.aspectj.weaver.tools.PointcutPrimitive; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -157,22 +157,19 @@ public class AspectJAdviceParameterNameDiscoverer implements ParameterNameDiscov /** The pointcut expression associated with the advice, as a simple String. */ - @Nullable - private final String pointcutExpression; + private final @Nullable String pointcutExpression; private boolean raiseExceptions; /** If the advice is afterReturning, and binds the return value, this is the parameter name used. */ - @Nullable - private String returningName; + private @Nullable String returningName; /** If the advice is afterThrowing, and binds the thrown value, this is the parameter name used. */ - @Nullable - private String throwingName; + private @Nullable String throwingName; private Class[] argumentTypes = new Class[0]; - private String[] parameterNameBindings = new String[0]; + private @Nullable String[] parameterNameBindings = new String[0]; private int numberOfRemainingUnboundArguments; @@ -221,8 +218,7 @@ public void setThrowingName(@Nullable String throwingName) { * @return the parameter names */ @Override - @Nullable - public String[] getParameterNames(Method method) { + public @Nullable String @Nullable [] getParameterNames(Method method) { this.argumentTypes = method.getParameterTypes(); this.numberOfRemainingUnboundArguments = this.argumentTypes.length; this.parameterNameBindings = new String[this.numberOfRemainingUnboundArguments]; @@ -241,7 +237,7 @@ public String[] getParameterNames(Method method) { try { int algorithmicStep = STEP_JOIN_POINT_BINDING; - while ((this.numberOfRemainingUnboundArguments > 0) && algorithmicStep < STEP_FINISHED) { + while (this.numberOfRemainingUnboundArguments > 0 && algorithmicStep < STEP_FINISHED) { switch (algorithmicStep++) { case STEP_JOIN_POINT_BINDING -> { if (!maybeBindThisJoinPoint()) { @@ -289,8 +285,7 @@ public String[] getParameterNames(Method method) { * {@link #setRaiseExceptions(boolean) raiseExceptions} has been set to {@code true} */ @Override - @Nullable - public String[] getParameterNames(Constructor ctor) { + public String @Nullable [] getParameterNames(Constructor ctor) { if (this.raiseExceptions) { throw new UnsupportedOperationException("An advice method can never be a constructor"); } @@ -373,7 +368,8 @@ private void maybeBindReturningVariable() { if (this.returningName != null) { if (this.numberOfRemainingUnboundArguments > 1) { throw new AmbiguousBindingException("Binding of returning parameter '" + this.returningName + - "' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates."); + "' is ambiguous: there are " + this.numberOfRemainingUnboundArguments + " candidates. " + + "Consider compiling with -parameters in order to make declared parameter names available."); } // We're all set... find the unbound parameter, and bind it. @@ -453,8 +449,7 @@ else if (numAnnotationSlots == 1) { /** * If the token starts meets Java identifier conventions, it's in. */ - @Nullable - private String maybeExtractVariableName(@Nullable String candidateToken) { + private @Nullable String maybeExtractVariableName(@Nullable String candidateToken) { if (AspectJProxyUtils.isVariableName(candidateToken)) { return candidateToken; } @@ -485,8 +480,8 @@ private void maybeExtractVariableNamesFromArgs(@Nullable String argsSpec, List 1) { - throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments - + " unbound args at this()/target()/args() binding stage, with no way to determine between them"); + throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments + + " unbound args at this()/target()/args() binding stage, with no way to determine between them"); } List varNames = new ArrayList<>(); @@ -535,8 +530,8 @@ else if (varNames.size() == 1) { private void maybeBindReferencePointcutParameter() { if (this.numberOfRemainingUnboundArguments > 1) { - throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments - + " unbound args at reference pointcut binding stage, with no way to determine between them"); + throw new AmbiguousBindingException("Still " + this.numberOfRemainingUnboundArguments + + " unbound args at reference pointcut binding stage, with no way to determine between them"); } List varNames = new ArrayList<>(); @@ -741,7 +736,9 @@ private void findAndBind(Class argumentType, String varName) { * Simple record to hold the extracted text from a pointcut body, together * with the number of tokens consumed in extracting it. */ - private record PointcutBody(int numTokensConsumed, @Nullable String text) {} + private record PointcutBody(int numTokensConsumed, @Nullable String text) { + } + /** * Thrown in response to an ambiguous binding being detected when diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java index a8081b461aa1..d0ed4f9f241d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterAdvice.java @@ -21,9 +21,9 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AfterAdvice; -import org.springframework.lang.Nullable; /** * Spring AOP advice wrapping an AspectJ after advice method. @@ -43,8 +43,7 @@ public AspectJAfterAdvice( @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { try { return mi.proceed(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.java index 48cedab1be7c..bcf9f5a8408d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterReturningAdvice.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. @@ -20,9 +20,10 @@ import java.lang.reflect.Method; import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.AfterAdvice; import org.springframework.aop.AfterReturningAdvice; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.TypeUtils; @@ -61,7 +62,7 @@ public void setReturningName(String name) { } @Override - public void afterReturning(@Nullable Object returnValue, Method method, Object[] args, @Nullable Object target) throws Throwable { + public void afterReturning(@Nullable Object returnValue, Method method, @Nullable Object[] args, @Nullable Object target) throws Throwable { if (shouldInvokeOnReturnValueOf(method, returnValue)) { invokeAdviceMethod(getJoinPointMatch(), returnValue, null); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java index 953658d66e50..4444f2175e3d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAfterThrowingAdvice.java @@ -21,9 +21,9 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AfterAdvice; -import org.springframework.lang.Nullable; /** * Spring AOP advice wrapping an AspectJ after-throwing advice method. @@ -58,8 +58,7 @@ public void setThrowingName(String name) { } @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { try { return mi.proceed(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java index 4ea59280d1b1..b97ef421a554 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAopUtils.java @@ -17,11 +17,11 @@ package org.springframework.aop.aspectj; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.AfterAdvice; import org.springframework.aop.BeforeAdvice; -import org.springframework.lang.Nullable; /** * Utility methods for dealing with AspectJ advisors. @@ -59,8 +59,7 @@ public static boolean isAfterAdvice(Advisor anAdvisor) { * If neither the advisor nor the advice have precedence information, this method * will return {@code null}. */ - @Nullable - public static AspectJPrecedenceInformation getAspectJPrecedenceInformationFor(Advisor anAdvisor) { + public static @Nullable AspectJPrecedenceInformation getAspectJPrecedenceInformationFor(Advisor anAdvisor) { if (anAdvisor instanceof AspectJPrecedenceInformation ajpi) { return ajpi; } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java index d1584c54af8a..bc3ca787da32 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJAroundAdvice.java @@ -23,9 +23,9 @@ import org.aopalliance.intercept.MethodInvocation; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.weaver.tools.JoinPointMatch; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.lang.Nullable; /** * Spring AOP around advice (MethodInterceptor) that wraps @@ -61,8 +61,7 @@ protected boolean supportsProceedingJoinPoint() { } @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } 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 aeb258e0a3b5..b1b196243bf8 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. @@ -39,6 +39,7 @@ import org.aspectj.weaver.tools.PointcutPrimitive; import org.aspectj.weaver.tools.ShadowMatch; import org.aspectj.weaver.tools.UnsupportedPointcutPrimitiveException; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; @@ -54,7 +55,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -99,8 +99,7 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut private static final Log logger = LogFactory.getLog(AspectJExpressionPointcut.class); - @Nullable - private Class pointcutDeclarationScope; + private @Nullable Class pointcutDeclarationScope; private boolean aspectCompiledByAjc; @@ -108,16 +107,13 @@ public class AspectJExpressionPointcut extends AbstractExpressionPointcut private Class[] pointcutParameterTypes = new Class[0]; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private transient ClassLoader pointcutClassLoader; + private transient volatile @Nullable ClassLoader pointcutClassLoader; - @Nullable - private transient PointcutExpression pointcutExpression; + private transient volatile @Nullable PointcutExpression pointcutExpression; - private transient boolean pointcutParsingFailed = false; + private transient volatile boolean pointcutParsingFailed; /** @@ -197,18 +193,20 @@ private void checkExpression() { * 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; } /** * Determine the ClassLoader to use for pointcut evaluation. */ - @Nullable - private ClassLoader determinePointcutClassLoader() { + private @Nullable ClassLoader determinePointcutClassLoader() { if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { return cbf.getBeanClassLoader(); } @@ -345,7 +343,7 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { ShadowMatch shadowMatch = getTargetShadowMatch(method, targetClass); // Bind Spring AOP proxy to AspectJ "this" and Spring AOP target to AspectJ target, @@ -403,8 +401,7 @@ public boolean matches(Method method, Class targetClass, Object... args) { } } - @Nullable - protected String getCurrentProxiedBeanName() { + protected @Nullable String getCurrentProxiedBeanName() { return ProxyCreationContext.getCurrentProxiedBeanName(); } @@ -412,8 +409,7 @@ protected String getCurrentProxiedBeanName() { /** * Get a new pointcut expression based on a target class's loader rather than the default. */ - @Nullable - private PointcutExpression getFallbackPointcutExpression(Class targetClass) { + private @Nullable PointcutExpression getFallbackPointcutExpression(Class targetClass) { try { ClassLoader classLoader = targetClass.getClassLoader(); if (classLoader != null && classLoader != this.pointcutClassLoader) { @@ -467,40 +463,24 @@ private ShadowMatch getTargetShadowMatch(Method method, Class targetClass) { } 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 { - 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; - } + PointcutExpression pointcutExpression = obtainPointcutExpression(); + synchronized (pointcutExpression) { + shadowMatch = ShadowMatchUtils.getShadowMatch(key); + if (shadowMatch != null) { + return shadowMatch; } - 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; + PointcutExpression fallbackExpression = null; + Method methodToMatch = targetMethod; + try { 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) { @@ -511,21 +491,45 @@ private ShadowMatch getShadowMatch(Method targetMethod, Method originalMethod) { 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; } @@ -623,14 +627,14 @@ public BeanContextMatcher(String expression) { @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); } @@ -720,4 +724,8 @@ public void setMatchingContext(MatchingContext aMatchContext) { } } + + private record ShadowMatchKey(AspectJExpressionPointcut expression, Method method) { + } + } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java index 9f4b1e990d8e..918a61ee5599 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJExpressionPointcutAdvisor.java @@ -16,11 +16,12 @@ package org.springframework.aop.aspectj; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Pointcut; import org.springframework.aop.support.AbstractGenericPointcutAdvisor; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; /** * Spring AOP Advisor that can be used for any AspectJ pointcut expression. @@ -38,8 +39,7 @@ public void setExpression(@Nullable String expression) { this.pointcut.setExpression(expression); } - @Nullable - public String getExpression() { + public @Nullable String getExpression() { return this.pointcut.getExpression(); } @@ -47,8 +47,7 @@ public void setLocation(@Nullable String location) { this.pointcut.setLocation(location); } - @Nullable - public String getLocation() { + public @Nullable String getLocation() { return this.pointcut.getLocation(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.java index 207291c51d5a..e8222966d3ca 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJMethodBeforeAdvice.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. @@ -19,8 +19,9 @@ import java.io.Serializable; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.MethodBeforeAdvice; -import org.springframework.lang.Nullable; /** * Spring AOP advice that wraps an AspectJ before method. @@ -40,7 +41,7 @@ public AspectJMethodBeforeAdvice( @Override - public void before(Method method, Object[] args, @Nullable Object target) throws Throwable { + public void before(Method method, @Nullable Object[] args, @Nullable Object target) throws Throwable { invokeAdviceMethod(getJoinPointMatch(), null, null); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java index 543146243ab0..94487cae882b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJPointcutAdvisor.java @@ -17,11 +17,11 @@ package org.springframework.aop.aspectj; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.aop.PointcutAdvisor; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -38,8 +38,7 @@ public class AspectJPointcutAdvisor implements PointcutAdvisor, Ordered { private final Pointcut pointcut; - @Nullable - private Integer order; + private @Nullable Integer order; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java index be7c8569404b..f09ec9d12ed1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/AspectJProxyUtils.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,10 +18,12 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Advisor; import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.StringUtils; /** @@ -75,6 +77,7 @@ private static boolean isAspectJAdvice(Advisor advisor) { pointcutAdvisor.getPointcut() instanceof AspectJExpressionPointcut)); } + @Contract("null -> false") static boolean isVariableName(@Nullable String name) { if (!StringUtils.hasLength(name)) { return false; 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 68eb55c9c4a6..d65035de7c08 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 @@ -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. @@ -25,11 +25,11 @@ import org.aspectj.lang.reflect.MethodSignature; import org.aspectj.lang.reflect.SourceLocation; import org.aspectj.runtime.internal.AroundClosure; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ProxyMethodInvocation; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -55,16 +55,13 @@ public class MethodInvocationProceedingJoinPoint implements ProceedingJoinPoint, private final ProxyMethodInvocation methodInvocation; - @Nullable - private Object[] args; + private @Nullable Object @Nullable [] args; /** Lazily initialized signature object. */ - @Nullable - private Signature signature; + private @Nullable Signature signature; /** Lazily initialized source location object. */ - @Nullable - private SourceLocation sourceLocation; + private @Nullable SourceLocation sourceLocation; /** @@ -84,14 +81,12 @@ public MethodInvocationProceedingJoinPoint(ProxyMethodInvocation methodInvocatio } @Override - @Nullable - public Object proceed() throws Throwable { + public @Nullable Object proceed() throws Throwable { return this.methodInvocation.invocableClone().proceed(); } @Override - @Nullable - public Object proceed(Object[] arguments) throws Throwable { + public @Nullable Object proceed(Object[] arguments) throws Throwable { Assert.notNull(arguments, "Argument array passed to proceed cannot be null"); if (arguments.length != this.methodInvocation.getArguments().length) { throw new IllegalArgumentException("Expecting " + @@ -114,13 +109,13 @@ public Object getThis() { * Returns the Spring AOP target. May be {@code null} if there is no target. */ @Override - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { return this.methodInvocation.getThis(); } @Override - public Object[] getArgs() { + @SuppressWarnings("NullAway") // Overridden method does not define nullness + public @Nullable Object[] getArgs() { if (this.args == null) { this.args = this.methodInvocation.getArguments().clone(); } @@ -180,8 +175,7 @@ public String toString() { */ private class MethodSignatureImpl implements MethodSignature { - @Nullable - private volatile String[] parameterNames; + private volatile @Nullable String @Nullable [] parameterNames; @Override public String getName() { @@ -219,9 +213,9 @@ public Class[] getParameterTypes() { } @Override - @Nullable - public String[] getParameterNames() { - String[] parameterNames = this.parameterNames; + @SuppressWarnings("NullAway") // Overridden method does not define nullness + public @Nullable String @Nullable [] getParameterNames() { + @Nullable String[] parameterNames = this.parameterNames; if (parameterNames == null) { parameterNames = parameterNameDiscoverer.getParameterNames(getMethod()); this.parameterNames = parameterNames; @@ -325,7 +319,7 @@ public int getLine() { } @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/RuntimeTestWalker.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java index bf37296a6e8a..3c34ea0e997b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/RuntimeTestWalker.java @@ -36,8 +36,8 @@ import org.aspectj.weaver.reflect.ReflectionVar; import org.aspectj.weaver.reflect.ShadowMatchImpl; import org.aspectj.weaver.tools.ShadowMatch; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -79,8 +79,7 @@ class RuntimeTestWalker { } - @Nullable - private final Test runtimeTest; + private final @Nullable Test runtimeTest; public RuntimeTestWalker(ShadowMatch shadowMatch) { 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 beb3ac63bb96..5e45c6d7d93c 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,60 +16,52 @@ 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.springframework.aop.support.ExpressionPointcut; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * 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); - /** - * Clear the cache of computed {@link ShadowMatch} instances. - */ - public static void clearCache() { - 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 + * 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 */ - @Nullable - static ShadowMatch getShadowMatch(ExpressionPointcut expression, Method method) { - return shadowMatchCache.get(new Key(expression, method)); + static @Nullable ShadowMatch getShadowMatch(Object key) { + return shadowMatchCache.get(key); } /** - * 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 + * 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 expression and method + * @return the shadow match to use for the specified key */ - static ShadowMatch setShadowMatch(ExpressionPointcut expression, Method method, ShadowMatch shadowMatch) { - ShadowMatch existing = shadowMatchCache.putIfAbsent(new Key(expression, method), shadowMatch); + static ShadowMatch setShadowMatch(Object key, ShadowMatch shadowMatch) { + ShadowMatch existing = shadowMatchCache.putIfAbsent(key, shadowMatch); return (existing != null ? existing : shadowMatch); } - - private record Key(ExpressionPointcut expression, Method method) {} + /** + * Clear the cache of computed {@link ShadowMatch} instances. + */ + public static void clearCache() { + shadowMatchCache.clear(); + } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java index f8a674ab13e6..1e2d45f4bf6a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/SimpleAspectInstanceFactory.java @@ -18,9 +18,10 @@ import java.lang.reflect.InvocationTargetException; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopConfigException; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -77,8 +78,7 @@ public final Object getAspectInstance() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { return this.aspectClass.getClassLoader(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java index 04edaa807663..a285945bc5d7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/SingletonAspectInstanceFactory.java @@ -18,8 +18,9 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -54,8 +55,7 @@ public final Object getAspectInstance() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { return this.aspectInstance.getClass().getClassLoader(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java index d6ddae267195..b3cff467db5e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/TypePatternClassFilter.java @@ -20,9 +20,9 @@ import org.aspectj.weaver.tools.PointcutParser; import org.aspectj.weaver.tools.TypePatternMatcher; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ClassFilter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -39,8 +39,7 @@ public class TypePatternClassFilter implements ClassFilter { private String typePattern = ""; - @Nullable - private TypePatternMatcher aspectJTypePatternMatcher; + private @Nullable TypePatternMatcher aspectJTypePatternMatcher; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java index 8400c54360a9..03a36ba11a5b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactory.java @@ -35,12 +35,12 @@ import org.aspectj.lang.reflect.AjType; import org.aspectj.lang.reflect.AjTypeSystem; import org.aspectj.lang.reflect.PerClauseKind; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopConfigException; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; /** * Abstract base class for factories that can create Spring AOP Advisors @@ -112,8 +112,7 @@ public void validate(Class aspectClass) throws AopConfigException { * (there should only be one anyway...). */ @SuppressWarnings("unchecked") - @Nullable - protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { + protected static @Nullable AspectJAnnotation findAspectJAnnotationOnMethod(Method method) { for (Class annotationType : ASPECTJ_ANNOTATION_CLASSES) { AspectJAnnotation annotation = findAnnotation(method, (Class) annotationType); if (annotation != null) { @@ -123,8 +122,7 @@ protected static AspectJAnnotation findAspectJAnnotationOnMethod(Method method) return null; } - @Nullable - private static AspectJAnnotation findAnnotation(Method method, Class annotationType) { + private static @Nullable AspectJAnnotation findAnnotation(Method method, Class annotationType) { Annotation annotation = AnnotationUtils.findAnnotation(method, annotationType); if (annotation != null) { return new AspectJAnnotation(annotation); @@ -242,8 +240,7 @@ private static class AspectJAnnotationParameterNameDiscoverer implements Paramet private static final String[] EMPTY_ARRAY = new String[0]; @Override - @Nullable - public String[] getParameterNames(Method method) { + public String @Nullable [] getParameterNames(Method method) { if (method.getParameterCount() == 0) { return EMPTY_ARRAY; } @@ -266,8 +263,7 @@ public String[] getParameterNames(Method method) { } @Override - @Nullable - public String[] getParameterNames(Constructor ctor) { + public @Nullable String @Nullable [] getParameterNames(Constructor ctor) { throw new UnsupportedOperationException("Spring AOP cannot handle constructor advice"); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java index 926392693e31..f53b1cdba5ee 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AnnotationAwareAspectJAutoProxyCreator.java @@ -20,11 +20,12 @@ import java.util.List; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Advisor; import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -49,14 +50,11 @@ @SuppressWarnings("serial") public class AnnotationAwareAspectJAutoProxyCreator extends AspectJAwareAdvisorAutoProxyCreator { - @Nullable - private List includePatterns; + private @Nullable List includePatterns; - @Nullable - private AspectJAdvisorFactory aspectJAdvisorFactory; + private @Nullable AspectJAdvisorFactory aspectJAdvisorFactory; - @Nullable - private BeanFactoryAspectJAdvisorsBuilder aspectJAdvisorsBuilder; + private @Nullable BeanFactoryAspectJAdvisorsBuilder aspectJAdvisorsBuilder; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java index 7149816f5742..6fa2b9105144 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessor.java @@ -18,13 +18,14 @@ import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -43,8 +44,7 @@ class AspectJAdvisorBeanRegistrationAotProcessor implements BeanRegistrationAotP @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { if (aspectjPresent) { Class beanClass = registeredBean.getBeanClass(); if (compiledByAjc(beanClass)) { @@ -74,7 +74,7 @@ public AspectJAdvisorContribution(Class beanClass) { @Override public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { - generationContext.getRuntimeHints().reflection().registerType(this.beanClass, MemberCategory.DECLARED_FIELDS); + generationContext.getRuntimeHints().reflection().registerType(this.beanClass, MemberCategory.ACCESS_DECLARED_FIELDS); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java index c3bf1685297e..31dcf53d68ce 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorFactory.java @@ -20,11 +20,11 @@ import java.util.List; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.aspectj.AspectJExpressionPointcut; import org.springframework.aop.framework.AopConfigException; -import org.springframework.lang.Nullable; /** * Interface for factories that can create Spring AOP Advisors from classes @@ -80,8 +80,7 @@ public interface AspectJAdvisorFactory { * or if it is a pointcut that will be used by other advice but will not * create a Spring advice in its own right */ - @Nullable - Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + @Nullable Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName); /** @@ -100,8 +99,7 @@ Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFact * @see org.springframework.aop.aspectj.AspectJAfterReturningAdvice * @see org.springframework.aop.aspectj.AspectJAfterThrowingAdvice */ - @Nullable - Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + @Nullable Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java index 71e7bea2b4ac..f859dc3f822b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessor.java @@ -18,6 +18,8 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Advisor; import org.springframework.aop.aspectj.AbstractAspectJAdvice; import org.springframework.aot.generate.GenerationContext; @@ -27,7 +29,6 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -45,8 +46,7 @@ class AspectJBeanFactoryInitializationAotProcessor implements BeanFactoryInitial @Override - @Nullable - public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { if (aspectJPresent) { return AspectDelegate.processAheadOfTime(beanFactory); } @@ -59,8 +59,7 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL */ private static class AspectDelegate { - @Nullable - private static AspectContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + private static @Nullable AspectContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { BeanFactoryAspectJAdvisorsBuilder builder = new BeanFactoryAspectJAdvisorsBuilder(beanFactory); List advisors = builder.buildAspectJAdvisors(); return (advisors.isEmpty() ? null : new AspectContribution(advisors)); diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java index 28d5aa13e50f..d0425b6e0e55 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectInstanceFactory.java @@ -18,11 +18,12 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.Ordered; import org.springframework.core.annotation.OrderUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -91,8 +92,7 @@ public Object getAspectInstance() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { return (this.beanFactory instanceof ConfigurableBeanFactory cbf ? cbf.getBeanClassLoader() : ClassUtils.getDefaultClassLoader()); } @@ -103,8 +103,7 @@ public AspectMetadata getAspectMetadata() { } @Override - @Nullable - public Object getAspectCreationMutex() { + public @Nullable Object getAspectCreationMutex() { if (this.beanFactory.isSingleton(this.name)) { // Rely on singleton semantics provided by the factory -> no local lock. return null; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java index 21bc248e508a..990b588ffd86 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/BeanFactoryAspectJAdvisorsBuilder.java @@ -25,12 +25,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.aspectj.lang.reflect.PerClauseKind; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.framework.AopConfigException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -49,8 +49,7 @@ public class BeanFactoryAspectJAdvisorsBuilder { private final AspectJAdvisorFactory advisorFactory; - @Nullable - private volatile List aspectBeanNames; + private volatile @Nullable List aspectBeanNames; private final Map> advisorsCache = new ConcurrentHashMap<>(); @@ -85,7 +84,6 @@ public BeanFactoryAspectJAdvisorsBuilder(ListableBeanFactory beanFactory, Aspect * @return the list of {@link org.springframework.aop.Advisor} beans * @see #isEligibleBean */ - @SuppressWarnings("NullAway") public List buildAspectJAdvisors() { List aspectNames = this.aspectBeanNames; @@ -159,6 +157,7 @@ public List buildAspectJAdvisors() { } else { MetadataAwareAspectInstanceFactory factory = this.aspectFactoryCache.get(aspectName); + Assert.state(factory != null, "Factory must not be null"); advisors.addAll(this.advisorFactory.getAdvisors(factory)); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java index db20f7608131..42b5349eac97 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/InstantiationModelAwarePointcutAdvisorImpl.java @@ -23,6 +23,7 @@ import org.aopalliance.aop.Advice; import org.aspectj.lang.reflect.PerClauseKind; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.aop.aspectj.AspectJExpressionPointcut; @@ -31,7 +32,6 @@ import org.springframework.aop.aspectj.annotation.AbstractAspectJAdvisorFactory.AspectJAnnotation; import org.springframework.aop.support.DynamicMethodMatcherPointcut; import org.springframework.aop.support.Pointcuts; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -73,13 +73,12 @@ final class InstantiationModelAwarePointcutAdvisorImpl private final boolean lazy; - @Nullable - private Advice instantiatedAdvice; + private @Nullable Advice instantiatedAdvice; - @Nullable + @SuppressWarnings("NullAway.Init") private Boolean isBeforeAdvice; - @Nullable + @SuppressWarnings("NullAway.Init") private Boolean isAfterAdvice; @@ -195,7 +194,6 @@ public int getDeclarationOrder() { } @Override - @SuppressWarnings("NullAway") public boolean isBeforeAdvice() { if (this.isBeforeAdvice == null) { determineAdviceType(); @@ -204,7 +202,6 @@ public boolean isBeforeAdvice() { } @Override - @SuppressWarnings("NullAway") public boolean isAfterAdvice() { if (this.isAfterAdvice == null) { determineAdviceType(); @@ -271,8 +268,7 @@ private static final class PerTargetInstantiationModelPointcut extends DynamicMe private final Pointcut preInstantiationPointcut; - @Nullable - private LazySingletonAspectInstanceFactoryDecorator aspectInstanceFactory; + private @Nullable LazySingletonAspectInstanceFactoryDecorator aspectInstanceFactory; public PerTargetInstantiationModelPointcut(AspectJExpressionPointcut declaredPointcut, Pointcut preInstantiationPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory) { @@ -293,7 +289,7 @@ public boolean matches(Method method, Class targetClass) { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { // This can match only on declared pointcut. return (isAspectMaterialized() && this.declaredPointcut.matches(method, targetClass, args)); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java index 73ba36c79dc3..a20fc06df8df 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/LazySingletonAspectInstanceFactoryDecorator.java @@ -18,7 +18,8 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -33,8 +34,7 @@ public class LazySingletonAspectInstanceFactoryDecorator implements MetadataAwar private final MetadataAwareAspectInstanceFactory maaif; - @Nullable - private volatile Object materialized; + private volatile @Nullable Object materialized; /** @@ -74,8 +74,7 @@ public boolean isMaterialized() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { return this.maaif.getAspectClassLoader(); } @@ -85,8 +84,7 @@ public AspectMetadata getAspectMetadata() { } @Override - @Nullable - public Object getAspectCreationMutex() { + public @Nullable Object getAspectCreationMutex() { return this.maaif.getAspectCreationMutex(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java index cb3e29baf49a..08c629108100 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/MetadataAwareAspectInstanceFactory.java @@ -16,8 +16,9 @@ package org.springframework.aop.aspectj.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.aspectj.AspectInstanceFactory; -import org.springframework.lang.Nullable; /** * Subinterface of {@link org.springframework.aop.aspectj.AspectInstanceFactory} @@ -41,7 +42,6 @@ public interface MetadataAwareAspectInstanceFactory extends AspectInstanceFactor * @return the mutex object (may be {@code null} for no mutex to use) * @since 4.3 */ - @Nullable - Object getAspectCreationMutex(); + @Nullable Object getAspectCreationMutex(); } 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 e4eec7a919d9..be8aa13e3e48 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 @@ -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.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.DeclareParents; import org.aspectj.lang.annotation.Pointcut; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.MethodBeforeAdvice; @@ -49,7 +50,6 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConvertingComparator; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils.MethodFilter; @@ -96,8 +96,7 @@ public class ReflectiveAspectJAdvisorFactory extends AbstractAspectJAdvisorFacto } - @Nullable - private final BeanFactory beanFactory; + private final @Nullable BeanFactory beanFactory; /** @@ -111,7 +110,7 @@ public ReflectiveAspectJAdvisorFactory() { * 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() @@ -183,8 +182,7 @@ private List getAdvisorMethods(Class aspectClass) { * @param introductionField the field to introspect * @return the Advisor instance, or {@code null} if not an Advisor */ - @Nullable - private Advisor getDeclareParentsAdvisor(Field introductionField) { + private @Nullable Advisor getDeclareParentsAdvisor(Field introductionField) { DeclareParents declareParents = introductionField.getAnnotation(DeclareParents.class); if (declareParents == null) { // Not an introduction field @@ -201,8 +199,7 @@ private Advisor getDeclareParentsAdvisor(Field introductionField) { @Override - @Nullable - public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, + public @Nullable Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrderInAspect, String aspectName) { validate(aspectInstanceFactory.getAspectMetadata().getAspectClass()); @@ -225,8 +222,7 @@ public Advisor getAdvisor(Method candidateAdviceMethod, MetadataAwareAspectInsta } } - @Nullable - private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { + private @Nullable AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Class candidateAspectClass) { AspectJAnnotation aspectJAnnotation = AbstractAspectJAdvisorFactory.findAspectJAnnotationOnMethod(candidateAdviceMethod); if (aspectJAnnotation == null) { @@ -244,8 +240,7 @@ private AspectJExpressionPointcut getPointcut(Method candidateAdviceMethod, Clas @Override - @Nullable - public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, + public @Nullable Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut expressionPointcut, MetadataAwareAspectInstanceFactory aspectInstanceFactory, int declarationOrder, String aspectName) { Class candidateAspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass(); @@ -307,7 +302,7 @@ public Advice getAdvice(Method candidateAdviceMethod, AspectJExpressionPointcut // Now to configure the advice... springAdvice.setAspectName(aspectName); springAdvice.setDeclarationOrder(declarationOrder); - String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); + @Nullable String[] argNames = this.parameterNameDiscoverer.getParameterNames(candidateAdviceMethod); if (argNames != null) { springAdvice.setArgumentNamesFromStringArray(argNames); } diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java index b5cf52470045..4f9573c2f779 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/annotation/package-info.java @@ -3,9 +3,7 @@ * *

Normally to be used through an AspectJAutoProxyCreator rather than directly. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.aspectj.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java index 255bfe961ccb..dce8139a1547 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/AspectJAwareAdvisorAutoProxyCreator.java @@ -101,7 +101,6 @@ protected void extendAdvisors(List candidateAdvisors) { @Override protected boolean shouldSkip(Class beanClass, String beanName) { - // TODO: Consider optimization by caching the list of the aspect names List candidateAdvisors = findCandidateAdvisors(); for (Advisor advisor : candidateAdvisors) { if (advisor instanceof AspectJPointcutAdvisor pointcutAdvisor && diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java index d83cd88d541f..65e6bf298d4b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/autoproxy/package-info.java @@ -2,9 +2,7 @@ * Base classes enabling auto-proxying based on AspectJ. * Support for AspectJ annotation aspects resides in the "aspectj.annotation" package. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.aspectj.autoproxy; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java b/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java index 2ffe8b16438b..45dce8a86a3d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/aspectj/package-info.java @@ -8,9 +8,7 @@ * or AspectJ load-time weaver. It is intended to enable the use of a valuable subset of AspectJ * functionality, with consistent semantics, with the proxy-based Spring AOP framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.aspectj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java index 25c8fa2c4d3c..6c53d9df0dda 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AdvisorComponentDefinition.java @@ -16,11 +16,12 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.parsing.AbstractComponentDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -110,8 +111,7 @@ public BeanReference[] getBeanReferences() { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.advisorDefinition.getSource(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java index 1bba8f1c2048..6ec5ea346061 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopConfigUtils.java @@ -19,6 +19,8 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.aspectj.annotation.AnnotationAwareAspectJAutoProxyCreator; import org.springframework.aop.aspectj.autoproxy.AspectJAwareAdvisorAutoProxyCreator; import org.springframework.aop.framework.autoproxy.InfrastructureAdvisorAutoProxyCreator; @@ -26,7 +28,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -64,37 +65,31 @@ public abstract class AopConfigUtils { } - @Nullable - public static BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { + public static @Nullable BeanDefinition registerAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { return registerAutoProxyCreatorIfNecessary(registry, null); } - @Nullable - public static BeanDefinition registerAutoProxyCreatorIfNecessary( + public static @Nullable BeanDefinition registerAutoProxyCreatorIfNecessary( BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(InfrastructureAdvisorAutoProxyCreator.class, registry, source); } - @Nullable - public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { + public static @Nullable BeanDefinition registerAspectJAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { return registerAspectJAutoProxyCreatorIfNecessary(registry, null); } - @Nullable - public static BeanDefinition registerAspectJAutoProxyCreatorIfNecessary( + public static @Nullable BeanDefinition registerAspectJAutoProxyCreatorIfNecessary( BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(AspectJAwareAdvisorAutoProxyCreator.class, registry, source); } - @Nullable - public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { + public static @Nullable BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary(BeanDefinitionRegistry registry) { return registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry, null); } - @Nullable - public static BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary( + public static @Nullable BeanDefinition registerAspectJAnnotationAutoProxyCreatorIfNecessary( BeanDefinitionRegistry registry, @Nullable Object source) { return registerOrEscalateApcAsRequired(AnnotationAwareAspectJAutoProxyCreator.class, registry, source); @@ -114,8 +109,7 @@ public static void forceAutoProxyCreatorToExposeProxy(BeanDefinitionRegistry reg } } - @Nullable - private static BeanDefinition registerOrEscalateApcAsRequired( + private static @Nullable BeanDefinition registerOrEscalateApcAsRequired( Class cls, BeanDefinitionRegistry registry, @Nullable Object source) { Assert.notNull(registry, "BeanDefinitionRegistry must not be null"); diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java index 5acb1cc5acd9..fca1bef8dd98 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AopNamespaceUtils.java @@ -16,13 +16,13 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * Utility class for handling registration of auto-proxy creators used internally diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java index 53d0d789a48d..7b283baad8a2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectComponentDefinition.java @@ -16,10 +16,11 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanReference; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; -import org.springframework.lang.Nullable; /** * {@link org.springframework.beans.factory.parsing.ComponentDefinition} @@ -38,8 +39,8 @@ public class AspectComponentDefinition extends CompositeComponentDefinition { private final BeanReference[] beanReferences; - public AspectComponentDefinition(String aspectName, @Nullable BeanDefinition[] beanDefinitions, - @Nullable BeanReference[] beanReferences, @Nullable Object source) { + public AspectComponentDefinition(String aspectName, BeanDefinition @Nullable [] beanDefinitions, + BeanReference @Nullable [] beanReferences, @Nullable Object source) { super(aspectName, source); this.beanDefinitions = (beanDefinitions != null ? beanDefinitions : new BeanDefinition[0]); diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.java index 93540fe11ddb..e0823a525d4c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectEntry.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,8 +46,8 @@ public AspectEntry(String id, String ref) { @Override public String toString() { - return "Aspect: " + (StringUtils.hasLength(this.id) ? "id='" + this.id + "'" - : "ref='" + this.ref + "'"); + return "Aspect: " + (StringUtils.hasLength(this.id) ? "id='" + this.id + "'" : + "ref='" + this.ref + "'"); } } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java index 70b9762006b0..27090155eb84 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/AspectJAutoProxyBeanDefinitionParser.java @@ -16,6 +16,7 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -25,7 +26,6 @@ import org.springframework.beans.factory.support.ManagedList; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * {@link BeanDefinitionParser} for the {@code aspectj-autoproxy} tag, @@ -39,8 +39,7 @@ class AspectJAutoProxyBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { AopNamespaceUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(parserContext, element); extendBeanDefinition(element, parserContext); return null; diff --git a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java index c13c6446a383..bcf4fc006e06 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/ConfigBeanDefinitionParser.java @@ -19,6 +19,7 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -45,7 +46,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -97,8 +97,7 @@ class ConfigBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), parserContext.extractSource(element)); parserContext.pushContainingComponent(compositeDef); @@ -453,8 +452,7 @@ private AbstractBeanDefinition parsePointcut(Element pointcutElement, ParserCont * {@link org.springframework.beans.factory.config.BeanDefinition} for the pointcut if necessary * and returns its bean name, otherwise returns the bean name of the referred pointcut. */ - @Nullable - private Object parsePointcutProperty(Element element, ParserContext parserContext) { + private @Nullable Object parsePointcutProperty(Element element, ParserContext parserContext) { if (element.hasAttribute(POINTCUT) && element.hasAttribute(POINTCUT_REF)) { parserContext.getReaderContext().error( "Cannot define both 'pointcut' and 'pointcut-ref' on tag.", diff --git a/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java index ebff6ee73e28..e48e5feb42a3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/MethodLocatingFactoryBean.java @@ -18,11 +18,12 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -33,14 +34,11 @@ */ public class MethodLocatingFactoryBean implements FactoryBean, BeanFactoryAware { - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; - @Nullable - private String methodName; + private @Nullable String methodName; - @Nullable - private Method method; + private @Nullable Method method; /** @@ -84,8 +82,7 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override - @Nullable - public Method getObject() throws Exception { + public @Nullable Method getObject() throws Exception { return this.method; } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java b/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java index 389a5b4216b8..f089a80d1918 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/PointcutComponentDefinition.java @@ -16,9 +16,10 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.AbstractComponentDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,8 +64,7 @@ public BeanDefinition[] getBeanDefinitions() { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.pointcutDefinition.getSource(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java index 446d5f93e0a8..4ad0100ceffc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/SimpleBeanFactoryAwareAspectInstanceFactory.java @@ -16,12 +16,13 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.aspectj.AspectInstanceFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -35,11 +36,9 @@ */ public class SimpleBeanFactoryAwareAspectInstanceFactory implements AspectInstanceFactory, BeanFactoryAware { - @Nullable - private String aspectBeanName; + private @Nullable String aspectBeanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** @@ -69,8 +68,7 @@ public Object getAspectInstance() { } @Override - @Nullable - public ClassLoader getAspectClassLoader() { + public @Nullable ClassLoader getAspectClassLoader() { if (this.beanFactory instanceof ConfigurableBeanFactory cbf) { return cbf.getBeanClassLoader(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java b/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java index f3adcd6a6718..c119ad2771ad 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/SpringConfiguredBeanDefinitionParser.java @@ -16,6 +16,7 @@ package org.springframework.aop.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -23,7 +24,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * {@link BeanDefinitionParser} responsible for parsing the @@ -52,8 +52,7 @@ class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(); def.setBeanClassName(BEAN_CONFIGURER_ASPECT_CLASS_NAME); diff --git a/spring-aop/src/main/java/org/springframework/aop/config/package-info.java b/spring-aop/src/main/java/org/springframework/aop/config/package-info.java index b0d1010cb327..5fb98e99ed8e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/config/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/config/package-info.java @@ -2,9 +2,7 @@ * Support package for declarative AOP configuration, * with XML schema being the primary configuration format. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java index 95eeda4a7a83..0eb8764cc2fa 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractAdvisingBeanPostProcessor.java @@ -19,12 +19,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Advisor; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; /** * Base class for {@link BeanPostProcessor} implementations that apply a @@ -37,8 +38,7 @@ public abstract class AbstractAdvisingBeanPostProcessor extends ProxyProcessorSupport implements SmartInstantiationAwareBeanPostProcessor { - @Nullable - protected Advisor advisor; + protected @Nullable Advisor advisor; protected boolean beforeExistingAdvisors = false; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java index fa1642835c0e..6e6f7aad5690 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AbstractSingletonProxyFactoryBean.java @@ -16,6 +16,8 @@ package org.springframework.aop.framework; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; @@ -24,7 +26,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -42,26 +43,20 @@ public abstract class AbstractSingletonProxyFactoryBean extends ProxyConfig implements FactoryBean, BeanClassLoaderAware, InitializingBean { - @Nullable - private Object target; + private @Nullable Object target; - @Nullable - private Class[] proxyInterfaces; + private Class @Nullable [] proxyInterfaces; - @Nullable - private Object[] preInterceptors; + private Object @Nullable [] preInterceptors; - @Nullable - private Object[] postInterceptors; + private Object @Nullable [] postInterceptors; /** Default is global AdvisorAdapterRegistry. */ private AdvisorAdapterRegistry advisorAdapterRegistry = GlobalAdvisorAdapterRegistry.getInstance(); - @Nullable - private transient ClassLoader proxyClassLoader; + private transient @Nullable ClassLoader proxyClassLoader; - @Nullable - private Object proxy; + private @Nullable Object proxy; /** @@ -221,8 +216,7 @@ public Object getObject() { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { if (this.proxy != null) { return this.proxy.getClass(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java index 707c98f26760..617dd7234e56 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisedSupport.java @@ -27,6 +27,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.DynamicIntroductionAdvice; @@ -40,7 +41,6 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.target.EmptyTargetSource; import org.springframework.aop.target.SingletonTargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -79,24 +79,28 @@ public class AdvisedSupport extends ProxyConfig implements Advised { /** Package-protected to allow direct access for efficiency. */ + @SuppressWarnings("serial") TargetSource targetSource = EMPTY_TARGET_SOURCE; /** Whether the Advisors are already filtered for the specific target class. */ private boolean preFiltered = false; /** The AdvisorChainFactory to use. */ + @SuppressWarnings("serial") private AdvisorChainFactory advisorChainFactory = DefaultAdvisorChainFactory.INSTANCE; /** * Interfaces to be implemented by the proxy. Held in List to keep the order * of registration, to create JDK proxy with specified order of interfaces. */ + @SuppressWarnings("serial") private List> interfaces = new ArrayList<>(); /** * List of Advisors. If an Advice is added, it will be wrapped * in an Advisor before being added to this List. */ + @SuppressWarnings("serial") private List advisors = new ArrayList<>(); /** @@ -105,15 +109,14 @@ public class AdvisedSupport extends ProxyConfig implements Advised { * @since 6.0.10 * @see #reduceToAdvisorKey */ + @SuppressWarnings("serial") private List advisorKey = this.advisors; /** Cache with Method as key and advisor chain List as value. */ - @Nullable - private transient Map> methodCache; + private transient @Nullable Map> methodCache; /** Cache with shared interceptors which are not method-specific. */ - @Nullable - private transient volatile List cachedInterceptors; + private transient volatile @Nullable List cachedInterceptors; /** * Optional field for {@link AopProxy} implementations to store metadata in. @@ -121,8 +124,7 @@ public class AdvisedSupport extends ProxyConfig implements Advised { * @since 6.1.3 * @see JdkDynamicAopProxy#JdkDynamicAopProxy(AdvisedSupport) */ - @Nullable - transient volatile Object proxyMetadataCache; + transient volatile @Nullable Object proxyMetadataCache; /** @@ -178,8 +180,7 @@ public void setTargetClass(@Nullable Class targetClass) { } @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetSource.getTargetClass(); } @@ -705,11 +706,9 @@ private static final class AdvisorKeyEntry implements Advisor { private final Class adviceType; - @Nullable - private final String classFilterKey; + private final @Nullable String classFilterKey; - @Nullable - private final String methodMatcherKey; + private final @Nullable String methodMatcherKey; public AdvisorKeyEntry(Advisor advisor) { this.adviceType = advisor.getAdvice().getClass(); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java index 3d31b8c7d481..947b059054e7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AdvisorChainFactory.java @@ -19,7 +19,7 @@ import java.lang.reflect.Method; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Factory interface for advisor chains. diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java index 9653ced6bc8b..cac3ea7785e0 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopContext.java @@ -16,8 +16,9 @@ package org.springframework.aop.framework; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; /** * Class containing static methods used to obtain information about the current AOP invocation. @@ -80,8 +81,7 @@ public static Object currentProxy() throws IllegalStateException { * @return the old proxy, which may be {@code null} if none was bound * @see #currentProxy() */ - @Nullable - static Object setCurrentProxy(@Nullable Object proxy) { + static @Nullable Object setCurrentProxy(@Nullable Object proxy) { Object old = currentProxy.get(); if (proxy != null) { currentProxy.set(proxy); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java index f103477504a1..b278e64f53cc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxy.java @@ -16,7 +16,7 @@ package org.springframework.aop.framework; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Delegate interface for a configured AOP proxy, allowing for the creation diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java index 26651f6200b8..92af32673341 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/AopProxyUtils.java @@ -23,13 +23,14 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.SpringProxy; import org.springframework.aop.TargetClassAware; import org.springframework.aop.TargetSource; import org.springframework.aop.support.AopUtils; import org.springframework.aop.target.SingletonTargetSource; import org.springframework.core.DecoratingProxy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -58,8 +59,7 @@ public abstract class AopProxyUtils { * @see Advised#getTargetSource() * @see SingletonTargetSource#getTarget() */ - @Nullable - public static Object getSingletonTarget(Object candidate) { + public static @Nullable Object getSingletonTarget(Object candidate) { if (candidate instanceof Advised advised) { TargetSource targetSource = advised.getTargetSource(); if (targetSource instanceof SingletonTargetSource singleTargetSource) { @@ -253,7 +253,7 @@ public static boolean equalsAdvisors(AdvisedSupport a, AdvisedSupport b) { * @return a cloned argument array, or the original if no adaptation is needed * @since 4.2.3 */ - static Object[] adaptArgumentsIfNecessary(Method method, @Nullable Object[] arguments) { + static @Nullable Object[] adaptArgumentsIfNecessary(Method method, @Nullable Object[] arguments) { if (ObjectUtils.isEmpty(arguments)) { return new Object[0]; } 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 aad0b4e9e0db..b770c9ecda23 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CglibAopProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,11 +30,13 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; 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; @@ -51,7 +53,6 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -113,11 +114,9 @@ class CglibAopProxy implements AopProxy, Serializable { /** The configuration used to configure this proxy. */ protected final AdvisedSupport advised; - @Nullable - protected Object[] constructorArgs; + protected Object @Nullable [] constructorArgs; - @Nullable - protected Class[] constructorArgTypes; + protected Class @Nullable [] constructorArgTypes; /** Dispatcher used for methods on Advised. */ private final transient AdvisedDispatcher advisedDispatcher; @@ -144,7 +143,7 @@ public CglibAopProxy(AdvisedSupport config) throws AopConfigException { * @param constructorArgs the constructor argument values * @param constructorArgTypes the constructor argument types */ - public void setConstructorArguments(@Nullable Object[] constructorArgs, @Nullable Class[] constructorArgTypes) { + public void setConstructorArguments(Object @Nullable [] constructorArgs, Class @Nullable [] constructorArgTypes) { if (constructorArgs == null || constructorArgTypes == null) { throw new IllegalArgumentException("Both 'constructorArgs' and 'constructorArgTypes' need to be specified"); } @@ -205,7 +204,7 @@ private Object buildProxy(@Nullable ClassLoader classLoader, boolean classOnly) enhancer.setSuperclass(proxySuperClass); enhancer.setInterfaces(AopProxyUtils.completeProxiedInterfaces(this.advised)); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(KotlinDetector.isKotlinType(proxySuperClass) ? new ClassLoaderAwareGeneratorStrategy(classLoader) : new ClassLoaderAwareGeneratorStrategy(classLoader, undeclaredThrowableStrategy) @@ -291,9 +290,15 @@ private void doValidateClass(Class proxySuperClass, @Nullable ClassLoader pro int mod = method.getModifiers(); if (!Modifier.isStatic(mod) && !Modifier.isPrivate(mod)) { if (Modifier.isFinal(mod)) { - if (logger.isWarnEnabled() && implementsInterface(method, ifcs)) { - logger.warn("Unable to proxy interface-implementing method [" + method + "] because " + - "it is marked as final, consider using interface-based JDK proxies instead."); + if (logger.isWarnEnabled() && Modifier.isPublic(mod)) { + if (implementsInterface(method, ifcs)) { + logger.warn("Unable to proxy interface-implementing method [" + method + "] because " + + "it is marked as final, consider using interface-based JDK proxies instead."); + } + else { + logger.warn("Public final method [" + method + "] cannot get proxied via CGLIB, " + + "consider removing the final marker or using interface-based JDK proxies."); + } } if (logger.isDebugEnabled()) { logger.debug("Final method [" + method + "] cannot get proxied via CGLIB: " + @@ -415,8 +420,7 @@ private static boolean implementsInterface(Method method, Set> ifcs) { * {@code proxy} and also verifies that {@code null} is not returned as a primitive. * Also takes care of the conversion from {@code Mono} to Kotlin Coroutines if needed. */ - @Nullable - private static Object processReturnType( + private static @Nullable Object processReturnType( Object proxy, @Nullable Object target, Method method, Object[] arguments, @Nullable Object returnValue) { // Massage return value if necessary @@ -455,16 +459,14 @@ public static class SerializableNoOp implements NoOp, Serializable { */ private static class StaticUnadvisedInterceptor implements MethodInterceptor, Serializable { - @Nullable - private final Object target; + private final @Nullable Object target; public StaticUnadvisedInterceptor(@Nullable Object target) { this.target = target; } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object retVal = AopUtils.invokeJoinpointUsingReflection(this.target, method, args); return processReturnType(proxy, this.target, method, args, retVal); } @@ -477,16 +479,14 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy */ private static class StaticUnadvisedExposedInterceptor implements MethodInterceptor, Serializable { - @Nullable - private final Object target; + private final @Nullable Object target; public StaticUnadvisedExposedInterceptor(@Nullable Object target) { this.target = target; } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; try { oldProxy = AopContext.setCurrentProxy(proxy); @@ -514,8 +514,7 @@ public DynamicUnadvisedInterceptor(TargetSource targetSource) { } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object target = this.targetSource.getTarget(); try { Object retVal = AopUtils.invokeJoinpointUsingReflection(target, method, args); @@ -542,8 +541,7 @@ public DynamicUnadvisedExposedInterceptor(TargetSource targetSource) { } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; Object target = this.targetSource.getTarget(); try { @@ -568,16 +566,14 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy */ private static class StaticDispatcher implements Dispatcher, Serializable { - @Nullable - private final Object target; + private final @Nullable Object target; public StaticDispatcher(@Nullable Object target) { this.target = target; } @Override - @Nullable - public Object loadObject() { + public @Nullable Object loadObject() { return this.target; } } @@ -655,11 +651,9 @@ private static class FixedChainStaticTargetInterceptor implements MethodIntercep private final List adviceChain; - @Nullable - private final Object target; + private final @Nullable Object target; - @Nullable - private final Class targetClass; + private final @Nullable Class targetClass; public FixedChainStaticTargetInterceptor( List adviceChain, @Nullable Object target, @Nullable Class targetClass) { @@ -670,8 +664,7 @@ public FixedChainStaticTargetInterceptor( } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { MethodInvocation invocation = new ReflectiveMethodInvocation( proxy, this.target, method, args, this.targetClass, this.adviceChain); // If we get here, we need to create a MethodInvocation. @@ -695,8 +688,7 @@ public DynamicAdvisedInterceptor(AdvisedSupport advised) { } @Override - @Nullable - public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { + public @Nullable Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; Object target = null; @@ -719,7 +711,7 @@ public Object intercept(Object proxy, Method method, Object[] args, MethodProxy // Note that the final invoker must be an InvokerInterceptor, so we know // it does nothing but a reflective operation on the target, and no hot // swapping or fancy proxying. - Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + @Nullable Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java index f1e06096161a..80c2fe245dc5 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/CoroutinesUtils.java @@ -19,10 +19,10 @@ import kotlin.coroutines.Continuation; import kotlinx.coroutines.reactive.ReactiveFlowKt; import kotlinx.coroutines.reactor.MonoKt; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; /** * Package-visible class designed to avoid a hard dependency on Kotlin and Coroutines dependency at runtime. @@ -41,9 +41,8 @@ static Object asFlow(@Nullable Object publisher) { } } - @Nullable @SuppressWarnings({"unchecked", "rawtypes"}) - static Object awaitSingleOrNull(@Nullable Object value, Object continuation) { + static @Nullable Object awaitSingleOrNull(@Nullable Object value, Object continuation) { return MonoKt.awaitSingleOrNull(value instanceof Mono mono ? mono : Mono.justOrEmpty(value), (Continuation) continuation); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java index 73c2fb430896..686a8df5513f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/DefaultAdvisorChainFactory.java @@ -24,6 +24,7 @@ import org.aopalliance.intercept.Interceptor; import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.IntroductionAdvisor; @@ -32,7 +33,6 @@ import org.springframework.aop.PointcutAdvisor; import org.springframework.aop.framework.adapter.AdvisorAdapterRegistry; import org.springframework.aop.framework.adapter.GlobalAdvisorAdapterRegistry; -import org.springframework.lang.Nullable; /** * A simple but definitive way of working out an advice chain for a Method, diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java index 3ab70ee9e877..4557f1e315dc 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/JdkDynamicAopProxy.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,6 +27,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AopInvocationException; import org.springframework.aop.RawTargetAccess; @@ -35,7 +36,6 @@ import org.springframework.core.DecoratingProxy; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -163,8 +163,7 @@ private ClassLoader determineClassLoader(@Nullable ClassLoader classLoader) { * unless a hook method throws an exception. */ @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object oldProxy = null; boolean setProxyContext = false; @@ -212,7 +211,7 @@ else if (!this.advised.opaque && method.getDeclaringClass().isInterface() && // We can skip creating a MethodInvocation: just invoke the target directly // Note that the final invoker must be an InvokerInterceptor so we know it does // nothing but a reflective operation on the target, and no hot swapping or fancy proxying. - Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); + @Nullable Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args); retVal = AopUtils.invokeJoinpointUsingReflection(target, method, argsToUse); } else { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java index 56330a6395a3..da05db588d86 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactory.java @@ -17,9 +17,9 @@ package org.springframework.aop.framework; import org.aopalliance.intercept.Interceptor; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java index 6556e530dfa3..fdec5887de87 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,6 +27,7 @@ import org.aopalliance.intercept.Interceptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.TargetSource; @@ -43,7 +44,6 @@ import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -99,13 +99,11 @@ public class ProxyFactoryBean extends ProxyCreatorSupport public static final String GLOBAL_SUFFIX = "*"; - protected final Log logger = LogFactory.getLog(getClass()); + private static final Log logger = LogFactory.getLog(ProxyFactoryBean.class); - @Nullable - private String[] interceptorNames; + private String @Nullable [] interceptorNames; - @Nullable - private String targetName; + private @Nullable String targetName; private boolean autodetectInterfaces = true; @@ -115,20 +113,17 @@ public class ProxyFactoryBean extends ProxyCreatorSupport private boolean freezeProxy = false; - @Nullable - private transient ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader(); + private transient @Nullable ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader(); private transient boolean classLoaderConfigured = false; - @Nullable - private transient BeanFactory beanFactory; + private transient @Nullable BeanFactory beanFactory; /** Whether the advisor chain has already been initialized. */ private boolean advisorChainInitialized = false; /** If this is a singleton, the cached singleton proxy instance. */ - @Nullable - private Object singletonInstance; + private @Nullable Object singletonInstance; /** @@ -246,8 +241,7 @@ public void setBeanFactory(BeanFactory beanFactory) { * @return a fresh AOP proxy reflecting the current state of this factory */ @Override - @Nullable - public Object getObject() throws BeansException { + public @Nullable Object getObject() throws BeansException { initializeAdvisorChain(); if (isSingleton()) { return getSingletonInstance(); @@ -268,8 +262,7 @@ public Object getObject() throws BeansException { * @see org.springframework.aop.framework.AopProxy#getProxyClass */ @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { synchronized (this) { if (this.singletonInstance != null) { return this.singletonInstance.getClass(); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java index f58e0be379f3..96f224c2c817 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ProxyProcessorSupport.java @@ -18,12 +18,13 @@ import java.io.Closeable; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -45,8 +46,7 @@ public class ProxyProcessorSupport extends ProxyConfig implements Ordered, BeanC */ private int order = Ordered.LOWEST_PRECEDENCE; - @Nullable - private ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader proxyClassLoader = ClassUtils.getDefaultClassLoader(); private boolean classLoaderConfigured = false; @@ -80,8 +80,7 @@ public void setProxyClassLoader(@Nullable ClassLoader classLoader) { /** * Return the configured proxy ClassLoader for this processor. */ - @Nullable - protected ClassLoader getProxyClassLoader() { + protected @Nullable ClassLoader getProxyClassLoader() { return this.proxyClassLoader; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java b/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java index b5ad8c36f1e0..eb07d5078430 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/ReflectiveMethodInvocation.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,11 +24,11 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ProxyMethodInvocation; import org.springframework.aop.support.AopUtils; import org.springframework.core.BridgeMethodResolver; -import org.springframework.lang.Nullable; /** * Spring's implementation of the AOP Alliance @@ -63,21 +63,18 @@ public class ReflectiveMethodInvocation implements ProxyMethodInvocation, Clonea protected final Object proxy; - @Nullable - protected final Object target; + protected final @Nullable Object target; protected final Method method; - protected Object[] arguments; + protected @Nullable Object[] arguments; - @Nullable - private final Class targetClass; + private final @Nullable Class targetClass; /** * Lazily initialized map of user-specific attributes for this invocation. */ - @Nullable - private Map userAttributes; + private @Nullable Map userAttributes; /** * List of MethodInterceptor and InterceptorAndDynamicMethodMatcher @@ -124,8 +121,7 @@ public final Object getProxy() { } @Override - @Nullable - public final Object getThis() { + public final @Nullable Object getThis() { return this.target; } @@ -145,19 +141,18 @@ public final Method getMethod() { } @Override - public final Object[] getArguments() { + public final @Nullable Object[] getArguments() { return this.arguments; } @Override - public void setArguments(Object... arguments) { + public void setArguments(@Nullable Object... arguments) { this.arguments = arguments; } @Override - @Nullable - public Object proceed() throws Throwable { + public @Nullable Object proceed() throws Throwable { // We start with an index of -1 and increment early. if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) { return invokeJoinpoint(); @@ -191,8 +186,7 @@ public Object proceed() throws Throwable { * @return the return value of the joinpoint * @throws Throwable if invoking the joinpoint resulted in an exception */ - @Nullable - protected Object invokeJoinpoint() throws Throwable { + protected @Nullable Object invokeJoinpoint() throws Throwable { return AopUtils.invokeJoinpointUsingReflection(this.target, this.method, this.arguments); } @@ -207,7 +201,7 @@ protected Object invokeJoinpoint() throws Throwable { */ @Override public MethodInvocation invocableClone() { - Object[] cloneArguments = this.arguments; + @Nullable Object[] cloneArguments = this.arguments; if (this.arguments.length > 0) { // Build an independent copy of the arguments array. cloneArguments = this.arguments.clone(); @@ -224,7 +218,7 @@ public MethodInvocation invocableClone() { * @see java.lang.Object#clone() */ @Override - public MethodInvocation invocableClone(Object... arguments) { + public MethodInvocation invocableClone(@Nullable Object... arguments) { // Force initialization of the user attributes Map, // for having a shared Map reference in the clone. if (this.userAttributes == null) { @@ -260,8 +254,7 @@ public void setUserAttribute(String key, @Nullable Object value) { } @Override - @Nullable - public Object getUserAttribute(String key) { + public @Nullable Object getUserAttribute(String key) { return (this.userAttributes != null ? this.userAttributes.get(key) : null); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java index 4ce1c45c87b2..108023045196 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/AfterReturningAdviceInterceptor.java @@ -20,10 +20,10 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AfterAdvice; import org.springframework.aop.AfterReturningAdvice; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -52,8 +52,7 @@ public AfterReturningAdviceInterceptor(AfterReturningAdvice advice) { @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { Object retVal = mi.proceed(); this.advice.afterReturning(retVal, mi.getMethod(), mi.getArguments(), mi.getThis()); return retVal; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java index 09683e02576e..a7a26a4288ef 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/MethodBeforeAdviceInterceptor.java @@ -20,10 +20,10 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.BeforeAdvice; import org.springframework.aop.MethodBeforeAdvice; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -52,8 +52,7 @@ public MethodBeforeAdviceInterceptor(MethodBeforeAdvice advice) { @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { this.advice.before(mi.getMethod(), mi.getArguments(), mi.getThis()); return mi.proceed(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java index 2baf3e93b140..b9d03e7b23bd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/ThrowsAdviceInterceptor.java @@ -25,10 +25,10 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.AfterAdvice; import org.springframework.aop.framework.AopConfigException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -131,8 +131,7 @@ public int getHandlerMethodCount() { @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { try { return mi.proceed(); } @@ -150,8 +149,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { * @param exception the exception thrown * @return a handler for the given exception type, or {@code null} if none found */ - @Nullable - private Method getExceptionHandler(Throwable exception) { + private @Nullable Method getExceptionHandler(Throwable exception) { Class exceptionClass = exception.getClass(); if (logger.isTraceEnabled()) { logger.trace("Trying to find handler for exception of type [" + exceptionClass.getName() + "]"); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java index 1925e47bfbc6..331af93b4ec6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/adapter/package-info.java @@ -9,9 +9,7 @@ * *

These adapters do not depend on any other Spring framework classes to allow such usage. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.framework.adapter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java index 7a71cb57569f..91364f18568b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAdvisorAutoProxyCreator.java @@ -18,6 +18,8 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Advisor; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.AopConfigException; @@ -26,7 +28,6 @@ import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -53,8 +54,7 @@ @SuppressWarnings("serial") public abstract class AbstractAdvisorAutoProxyCreator extends AbstractAutoProxyCreator { - @Nullable - private BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper; + private @Nullable BeanFactoryAdvisorRetrievalHelper advisorRetrievalHelper; @Override @@ -73,8 +73,7 @@ protected void initBeanFactory(ConfigurableListableBeanFactory beanFactory) { @Override - @Nullable - protected Object[] getAdvicesAndAdvisorsForBean( + protected Object @Nullable [] getAdvicesAndAdvisorsForBean( Class beanClass, String beanName, @Nullable TargetSource targetSource) { List advisors = findEligibleAdvisors(beanClass, beanName); diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java index ffdf6173f126..cb60fccd7261 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractAutoProxyCreator.java @@ -28,6 +28,7 @@ import org.aopalliance.aop.Advice; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.Pointcut; @@ -48,7 +49,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -101,8 +101,7 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport * Convenience constant for subclasses: Return value for "do not proxy". * @see #getAdvicesAndAdvisorsForBean */ - @Nullable - protected static final Object[] DO_NOT_PROXY = null; + protected static final Object @Nullable [] DO_NOT_PROXY = null; /** * Convenience constant for subclasses: Return value for @@ -129,11 +128,9 @@ public abstract class AbstractAutoProxyCreator extends ProxyProcessorSupport private boolean applyCommonInterceptorsFirst = true; - @Nullable - private TargetSourceCreator[] customTargetSourceCreators; + private TargetSourceCreator @Nullable [] customTargetSourceCreators; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; private final Set targetSourcedBeans = ConcurrentHashMap.newKeySet(16); @@ -215,15 +212,13 @@ public void setBeanFactory(BeanFactory beanFactory) { * Return the owning {@link BeanFactory}. * May be {@code null}, as this post-processor doesn't need to belong to a bean factory. */ - @Nullable - protected BeanFactory getBeanFactory() { + protected @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } @Override - @Nullable - public Class predictBeanType(Class beanClass, String beanName) { + public @Nullable Class predictBeanType(Class beanClass, String beanName) { if (this.proxyTypes.isEmpty()) { return null; } @@ -256,8 +251,7 @@ public Class determineBeanType(Class beanClass, String beanName) { } @Override - @Nullable - public Constructor[] determineCandidateConstructors(Class beanClass, String beanName) { + public Constructor @Nullable [] determineCandidateConstructors(Class beanClass, String beanName) { return null; } @@ -269,8 +263,7 @@ public Object getEarlyBeanReference(Object bean, String beanName) { } @Override - @Nullable - public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + public @Nullable Object postProcessBeforeInstantiation(Class beanClass, String beanName) { Object cacheKey = getCacheKey(beanClass, beanName); if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) { @@ -311,8 +304,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str * @see #getAdvicesAndAdvisorsForBean */ @Override - @Nullable - public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { + public @Nullable Object postProcessAfterInitialization(@Nullable Object bean, String beanName) { if (bean != null) { Object cacheKey = getCacheKey(bean.getClass(), beanName); if (this.earlyBeanReferences.remove(cacheKey) != bean) { @@ -426,8 +418,7 @@ protected boolean shouldSkip(Class beanClass, String beanName) { * @return a TargetSource for this bean * @see #setCustomTargetSourceCreators */ - @Nullable - protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + protected @Nullable TargetSource getCustomTargetSource(Class beanClass, String beanName) { // We can't create fancy target sources for directly registered singletons. if (this.customTargetSourceCreators != null && this.beanFactory != null && this.beanFactory.containsBean(beanName)) { @@ -460,19 +451,19 @@ protected TargetSource getCustomTargetSource(Class beanClass, String beanName * @see #buildAdvisors */ protected Object createProxy(Class beanClass, @Nullable String beanName, - @Nullable Object[] specificInterceptors, TargetSource targetSource) { + Object @Nullable [] specificInterceptors, TargetSource targetSource) { return buildProxy(beanClass, beanName, specificInterceptors, targetSource, false); } private Class createProxyClass(Class beanClass, @Nullable String beanName, - @Nullable Object[] specificInterceptors, TargetSource targetSource) { + Object @Nullable [] specificInterceptors, TargetSource targetSource) { return (Class) buildProxy(beanClass, beanName, specificInterceptors, targetSource, true); } private Object buildProxy(Class beanClass, @Nullable String beanName, - @Nullable Object[] specificInterceptors, TargetSource targetSource, boolean classOnly) { + Object @Nullable [] specificInterceptors, TargetSource targetSource, boolean classOnly) { if (this.beanFactory instanceof ConfigurableListableBeanFactory clbf) { AutoProxyUtils.exposeTargetClass(clbf, beanName, beanClass); @@ -554,7 +545,7 @@ protected boolean advisorsPreFiltered() { * specific to this bean (may be empty, but not null) * @return the list of Advisors for the given bean */ - protected Advisor[] buildAdvisors(@Nullable String beanName, @Nullable Object[] specificInterceptors) { + protected Advisor[] buildAdvisors(@Nullable String beanName, Object @Nullable [] specificInterceptors) { // Handle prototypes correctly... Advisor[] commonInterceptors = resolveInterceptorNames(); @@ -633,8 +624,7 @@ protected void customizeProxyFactory(ProxyFactory proxyFactory) { * @see #DO_NOT_PROXY * @see #PROXY_WITHOUT_ADDITIONAL_INTERCEPTORS */ - @Nullable - protected abstract Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, + protected abstract Object @Nullable [] getAdvicesAndAdvisorsForBean(Class beanClass, String beanName, @Nullable TargetSource customTargetSource) throws BeansException; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java index 0dbea8a467f5..3cba6a7fd5e4 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AbstractBeanFactoryAwareAdvisingPostProcessor.java @@ -16,12 +16,13 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AbstractAdvisingBeanPostProcessor; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; /** * Extension of {@link AbstractAutoProxyCreator} which implements {@link BeanFactoryAware}, @@ -40,8 +41,7 @@ public abstract class AbstractBeanFactoryAwareAdvisingPostProcessor extends AbstractAdvisingBeanPostProcessor implements BeanFactoryAware { - @Nullable - private ConfigurableListableBeanFactory beanFactory; + private @Nullable ConfigurableListableBeanFactory beanFactory; @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java index 92c25b432d5d..0208b14e5184 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/AutoProxyUtils.java @@ -16,11 +16,12 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.Conventions; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -84,8 +85,7 @@ public static boolean shouldProxyTargetClass( * @since 4.2.3 * @see org.springframework.beans.factory.BeanFactory#getType(String) */ - @Nullable - public static Class determineTargetClass( + public static @Nullable Class determineTargetClass( ConfigurableListableBeanFactory beanFactory, @Nullable String beanName) { if (beanName == null) { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java index a82a8bce56d5..4ff290ec3484 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/BeanFactoryAdvisorRetrievalHelper.java @@ -21,13 +21,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCurrentlyInCreationException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -44,8 +44,7 @@ public class BeanFactoryAdvisorRetrievalHelper { private final ConfigurableListableBeanFactory beanFactory; - @Nullable - private volatile String[] cachedAdvisorBeanNames; + private volatile String @Nullable [] cachedAdvisorBeanNames; /** 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 27aa0547363c..a9a3f88a9199 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. @@ -19,10 +19,11 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PatternMatchUtils; @@ -48,8 +49,7 @@ public class BeanNameAutoProxyCreator extends AbstractAutoProxyCreator { private static final String[] NO_ALIASES = new String[0]; - @Nullable - private List beanNames; + private @Nullable List beanNames; /** @@ -81,8 +81,7 @@ public void setBeanNames(String... beanNames) { * @see #setBeanNames(String...) */ @Override - @Nullable - protected TargetSource getCustomTargetSource(Class beanClass, String beanName) { + protected @Nullable TargetSource getCustomTargetSource(Class beanClass, String beanName) { return (isSupportedBeanName(beanClass, beanName) ? super.getCustomTargetSource(beanClass, beanName) : null); } @@ -93,8 +92,7 @@ protected TargetSource getCustomTargetSource(Class beanClass, String beanName * @see #setBeanNames(String...) */ @Override - @Nullable - protected Object[] getAdvicesAndAdvisorsForBean( + protected Object @Nullable [] getAdvicesAndAdvisorsForBean( Class beanClass, String beanName, @Nullable TargetSource targetSource) { return (isSupportedBeanName(beanClass, beanName) ? @@ -114,10 +112,10 @@ private boolean isSupportedBeanName(Class beanClass, String beanName) { 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/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java index 07aff4a3f91d..98122847d27c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/DefaultAdvisorAutoProxyCreator.java @@ -16,8 +16,9 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanNameAware; -import org.springframework.lang.Nullable; /** * {@code BeanPostProcessor} implementation that creates AOP proxies based on all @@ -44,8 +45,7 @@ public class DefaultAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCrea private boolean usePrefix = false; - @Nullable - private String advisorBeanNamePrefix; + private @Nullable String advisorBeanNamePrefix; /** @@ -78,8 +78,7 @@ public void setAdvisorBeanNamePrefix(@Nullable String advisorBeanNamePrefix) { * Return the prefix for bean names that will cause them to be included * for auto-proxying by this object. */ - @Nullable - public String getAdvisorBeanNamePrefix() { + public @Nullable String getAdvisorBeanNamePrefix() { return this.advisorBeanNamePrefix; } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java index f283920fca76..0c11bc8c228e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/InfrastructureAdvisorAutoProxyCreator.java @@ -16,9 +16,10 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; /** * Auto-proxy creator that considers infrastructure Advisor beans only, @@ -30,8 +31,7 @@ @SuppressWarnings("serial") public class InfrastructureAdvisorAutoProxyCreator extends AbstractAdvisorAutoProxyCreator { - @Nullable - private ConfigurableListableBeanFactory beanFactory; + private @Nullable ConfigurableListableBeanFactory beanFactory; @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java index 314fcc98f236..19a30ad6c7cf 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/ProxyCreationContext.java @@ -16,8 +16,9 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; /** * Holder for the current proxy creation context, as exposed by auto-proxy creators @@ -42,8 +43,7 @@ private ProxyCreationContext() { * Return the name of the currently proxied bean instance. * @return the name of the bean, or {@code null} if none available */ - @Nullable - public static String getCurrentProxiedBeanName() { + public static @Nullable String getCurrentProxiedBeanName() { return currentProxiedBeanName.get(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java index 012c060e98d5..e8a88d2d5b79 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/TargetSourceCreator.java @@ -16,8 +16,9 @@ package org.springframework.aop.framework.autoproxy; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; /** * Implementations can create special target sources, such as pooling target @@ -40,7 +41,6 @@ public interface TargetSourceCreator { * @return a special TargetSource or {@code null} if this TargetSourceCreator isn't * interested in the particular bean */ - @Nullable - TargetSource getTargetSource(Class beanClass, String beanName); + @Nullable TargetSource getTargetSource(Class beanClass, String beanName); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java index 328312146ade..15acaaff35b9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/package-info.java @@ -9,9 +9,7 @@ * as post-processors beans are only automatically detected in application contexts. * Post-processors can be explicitly registered on a ConfigurableBeanFactory instead. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.framework.autoproxy; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java index b9a5e4e4c422..4fd9054084ea 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/AbstractBeanFactoryBasedTargetSourceCreator.java @@ -21,6 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.AopInfrastructureBean; @@ -33,7 +34,6 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -59,8 +59,7 @@ public abstract class AbstractBeanFactoryBasedTargetSourceCreator protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private ConfigurableBeanFactory beanFactory; + private @Nullable ConfigurableBeanFactory beanFactory; /** Internally used DefaultListableBeanFactory instances, keyed by bean name. */ private final Map internalBeanFactories = new HashMap<>(); @@ -78,8 +77,7 @@ public final void setBeanFactory(BeanFactory beanFactory) { /** * Return the BeanFactory that this TargetSourceCreators runs in. */ - @Nullable - protected final BeanFactory getBeanFactory() { + protected final @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } @@ -94,8 +92,7 @@ private ConfigurableBeanFactory getConfigurableBeanFactory() { //--------------------------------------------------------------------- @Override - @Nullable - public final TargetSource getTargetSource(Class beanClass, String beanName) { + public final @Nullable TargetSource getTargetSource(Class beanClass, String beanName) { AbstractBeanFactoryBasedTargetSource targetSource = createBeanFactoryBasedTargetSource(beanClass, beanName); if (targetSource == null) { @@ -195,8 +192,7 @@ protected boolean isPrototypeBased() { * @param beanName the name of the bean * @return the AbstractPrototypeBasedTargetSource, or {@code null} if we don't match this */ - @Nullable - protected abstract AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( + protected abstract @Nullable AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( Class beanClass, String beanName); } diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/LazyInitTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/LazyInitTargetSourceCreator.java index 68ca0524471a..bca5d66424d6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/LazyInitTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/LazyInitTargetSourceCreator.java @@ -16,11 +16,12 @@ package org.springframework.aop.framework.autoproxy.target; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource; import org.springframework.aop.target.LazyInitTargetSource; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; /** * {@code TargetSourceCreator} that enforces a {@link LazyInitTargetSource} for @@ -62,8 +63,7 @@ protected boolean isPrototypeBased() { } @Override - @Nullable - protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( + protected @Nullable AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( Class beanClass, String beanName) { if (getBeanFactory() instanceof ConfigurableListableBeanFactory clbf) { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java index f7df6c30249b..18836564c6e6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/QuickTargetSourceCreator.java @@ -16,11 +16,12 @@ package org.springframework.aop.framework.autoproxy.target; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.target.AbstractBeanFactoryBasedTargetSource; import org.springframework.aop.target.CommonsPool2TargetSource; import org.springframework.aop.target.PrototypeTargetSource; import org.springframework.aop.target.ThreadLocalTargetSource; -import org.springframework.lang.Nullable; /** * Convenient TargetSourceCreator using bean name prefixes to create one of three @@ -55,8 +56,7 @@ public class QuickTargetSourceCreator extends AbstractBeanFactoryBasedTargetSour public static final String PREFIX_PROTOTYPE = "!"; @Override - @Nullable - protected final AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( + protected final @Nullable AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource( Class beanClass, String beanName) { if (beanName.startsWith(PREFIX_COMMONS_POOL)) { diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java index 2e0608db9d2c..928aa745b36b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/autoproxy/target/package-info.java @@ -2,9 +2,7 @@ * Various {@link org.springframework.aop.framework.autoproxy.TargetSourceCreator} * implementations for use with Spring's AOP auto-proxying support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.framework.autoproxy.target; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java b/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java index c05af5dea98a..db79833a4750 100644 --- a/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/framework/package-info.java @@ -12,9 +12,7 @@ * or ApplicationContext. However, proxies can be created programmatically using the * ProxyFactory class. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.framework; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java index 536e6e3ade39..a9e7d6ca11e6 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractMonitoringInterceptor.java @@ -19,8 +19,7 @@ import java.lang.reflect.Method; import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Base class for monitoring interceptors, such as performance monitors. diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java index 2d67708c351a..8ff8cbd6197e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AbstractTraceInterceptor.java @@ -22,9 +22,9 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -52,8 +52,7 @@ public abstract class AbstractTraceInterceptor implements MethodInterceptor, Ser * The default {@code Log} instance used to write trace messages. * This instance is mapped to the implementing {@code Class}. */ - @Nullable - protected transient Log defaultLogger = LogFactory.getLog(getClass()); + protected transient @Nullable Log defaultLogger = LogFactory.getLog(getClass()); /** * Indicates whether proxy class names should be hidden when using dynamic loggers. @@ -125,8 +124,7 @@ public void setLogExceptionStackTrace(boolean logExceptionStackTrace) { * @see #invokeUnderTrace(org.aopalliance.intercept.MethodInvocation, org.apache.commons.logging.Log) */ @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Log logger = getLoggerForInvocation(invocation); if (isInterceptorEnabled(invocation, logger)) { return invokeUnderTrace(invocation, logger); @@ -245,7 +243,6 @@ protected void writeToLog(Log logger, String message, @Nullable Throwable ex) { * @see #writeToLog(Log, String) * @see #writeToLog(Log, String, Throwable) */ - @Nullable - protected abstract Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable; + protected abstract @Nullable Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable; } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java index ea5034799622..26d07c277333 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionAspectSupport.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,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -38,7 +39,6 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; import org.springframework.core.task.support.TaskExecutorAdapter; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -78,11 +78,9 @@ public abstract class AsyncExecutionAspectSupport implements BeanFactoryAware { private SingletonSupplier exceptionHandler; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; private final Map executors = new ConcurrentHashMap<>(16); @@ -118,8 +116,8 @@ public AsyncExecutionAspectSupport(@Nullable Executor defaultExecutor, AsyncUnca * applying the corresponding default if a supplier is not resolvable. * @since 5.1 */ - public void configure(@Nullable Supplier defaultExecutor, - @Nullable Supplier exceptionHandler) { + public void configure(@Nullable Supplier defaultExecutor, + @Nullable Supplier exceptionHandler) { this.defaultExecutor = new SingletonSupplier<>(defaultExecutor, () -> getDefaultExecutor(this.beanFactory)); this.exceptionHandler = new SingletonSupplier<>(exceptionHandler, SimpleAsyncUncaughtExceptionHandler::new); @@ -167,8 +165,7 @@ public void setBeanFactory(BeanFactory beanFactory) { * Determine the specific executor to use when executing the given method. * @return the executor to use (or {@code null}, but just if no default executor is available) */ - @Nullable - protected AsyncTaskExecutor determineAsyncExecutor(Method method) { + protected @Nullable AsyncTaskExecutor determineAsyncExecutor(Method method) { AsyncTaskExecutor executor = this.executors.get(method); if (executor == null) { Executor targetExecutor; @@ -203,8 +200,7 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { * @see #determineAsyncExecutor(Method) * @see #findQualifiedExecutor(BeanFactory, String) */ - @Nullable - protected abstract String getExecutorQualifier(Method method); + protected abstract @Nullable String getExecutorQualifier(Method method); /** * Retrieve a target executor for the given qualifier. @@ -213,8 +209,7 @@ protected AsyncTaskExecutor determineAsyncExecutor(Method method) { * @since 4.2.6 * @see #getExecutorQualifier(Method) */ - @Nullable - protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, String qualifier) { + protected @Nullable Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, String qualifier) { if (beanFactory == null) { throw new IllegalStateException("BeanFactory must be set on " + getClass().getSimpleName() + " to access qualified executor '" + qualifier + "'"); @@ -234,8 +229,7 @@ protected Executor findQualifiedExecutor(@Nullable BeanFactory beanFactory, Stri * @see #findQualifiedExecutor(BeanFactory, String) * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME */ - @Nullable - protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + protected @Nullable Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { if (beanFactory != null) { try { // Search for TaskExecutor bean... not plain Executor since that would @@ -281,15 +275,10 @@ protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { * @param returnType the declared return type (potentially a {@link Future} variant) * @return the execution result (potentially a corresponding {@link Future} handle) */ - @SuppressWarnings("removal") - @Nullable - protected Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { + protected @Nullable Object doSubmit(Callable task, AsyncTaskExecutor executor, Class returnType) { if (CompletableFuture.class.isAssignableFrom(returnType)) { return executor.submitCompletable(task); } - else if (org.springframework.util.concurrent.ListenableFuture.class.isAssignableFrom(returnType)) { - return ((org.springframework.core.task.AsyncListenableTaskExecutor) executor).submitListenable(task); - } else if (Future.class.isAssignableFrom(returnType)) { return executor.submit(task); } @@ -315,7 +304,7 @@ else if (void.class == returnType || "kotlin.Unit".equals(returnType.getName())) * @param method the method that was invoked * @param params the parameters used to invoke the method */ - protected void handleError(Throwable ex, Method method, Object... params) throws Exception { + protected void handleError(Throwable ex, Method method, @Nullable Object... params) throws Exception { if (Future.class.isAssignableFrom(method.getReturnType())) { ReflectionUtils.rethrowException(ex); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java index 12e073db035f..74de867f4bc3 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncExecutionInterceptor.java @@ -24,6 +24,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactory; @@ -31,7 +32,6 @@ import org.springframework.core.Ordered; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.SimpleAsyncTaskExecutor; -import org.springframework.lang.Nullable; /** * AOP Alliance {@code MethodInterceptor} that processes method invocations @@ -97,9 +97,7 @@ public AsyncExecutionInterceptor(@Nullable Executor defaultExecutor, AsyncUncaug * otherwise. */ @Override - @Nullable - @SuppressWarnings("NullAway") - public Object invoke(final MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable { Class targetClass = (invocation.getThis() != null ? AopUtils.getTargetClass(invocation.getThis()) : null); final Method userMethod = BridgeMethodResolver.getMostSpecificMethod(invocation.getMethod(), targetClass); @@ -117,7 +115,8 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { } } catch (ExecutionException ex) { - handleError(ex.getCause(), userMethod, invocation.getArguments()); + Throwable cause = ex.getCause(); + handleError(cause == null ? ex : cause, userMethod, invocation.getArguments()); } catch (Throwable ex) { handleError(ex, userMethod, invocation.getArguments()); @@ -140,8 +139,7 @@ public Object invoke(final MethodInvocation invocation) throws Throwable { * @see #determineAsyncExecutor(Method) */ @Override - @Nullable - protected String getExecutorQualifier(Method method) { + protected @Nullable String getExecutorQualifier(Method method) { return null; } @@ -154,8 +152,7 @@ protected String getExecutorQualifier(Method method) { * @see #DEFAULT_TASK_EXECUTOR_BEAN_NAME */ @Override - @Nullable - protected Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { + protected @Nullable Executor getDefaultExecutor(@Nullable BeanFactory beanFactory) { Executor defaultExecutor = super.getDefaultExecutor(beanFactory); return (defaultExecutor != null ? defaultExecutor : new SimpleAsyncTaskExecutor()); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java index 868e4898f5e0..5b80e745b2fa 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/AsyncUncaughtExceptionHandler.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + /** * A strategy for handling uncaught exceptions thrown from asynchronous methods. * @@ -38,6 +40,6 @@ public interface AsyncUncaughtExceptionHandler { * @param method the asynchronous method * @param params the parameters used to invoke the method */ - void handleUncaughtException(Throwable ex, Method method, Object... params); + void handleUncaughtException(Throwable ex, Method method, @Nullable Object... params); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java index 3e6e6e9b7c5b..65d540f939cd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ConcurrencyThrottleInterceptor.java @@ -20,8 +20,8 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ConcurrencyThrottleSupport; /** @@ -49,8 +49,7 @@ public ConcurrencyThrottleInterceptor() { } @Override - @Nullable - public Object invoke(MethodInvocation methodInvocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation methodInvocation) throws Throwable { beforeAccess(); try { return methodInvocation.proceed(); diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java index 46c879ff1d5c..314c82b7306e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/CustomizableTraceInterceptor.java @@ -22,8 +22,8 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StopWatch; @@ -251,7 +251,7 @@ public void setExceptionMessage(String exceptionMessage) { * @see #setExceptionMessage */ @Override - protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + protected @Nullable Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { String name = ClassUtils.getQualifiedMethodName(invocation.getMethod()); StopWatch stopWatch = new StopWatch(name); Object returnValue = null; diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java index 06ea6102909e..288771bb9a8a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/DebugInterceptor.java @@ -17,8 +17,7 @@ package org.springframework.aop.interceptor; import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * AOP Alliance {@code MethodInterceptor} that can be introduced in a chain @@ -58,8 +57,7 @@ public DebugInterceptor(boolean useDynamicLogger) { @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { synchronized (this) { this.count++; } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java index 1f095ed89e9e..f43a9ee8d20b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeBeanNameAdvisors.java @@ -18,6 +18,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.ProxyMethodInvocation; @@ -25,7 +26,6 @@ import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.aop.support.DelegatingIntroductionInterceptor; import org.springframework.beans.factory.NamedBean; -import org.springframework.lang.Nullable; /** * Convenient methods for creating advisors that may be used when autoproxying beans @@ -110,8 +110,7 @@ public ExposeBeanNameInterceptor(String beanName) { } @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } @@ -134,8 +133,7 @@ public ExposeBeanNameIntroduction(String beanName) { } @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { if (!(mi instanceof ProxyMethodInvocation pmi)) { throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java index 18c9caf1ea87..a3a7bca9acde 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/ExposeInvocationInterceptor.java @@ -20,12 +20,12 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.core.NamedThreadLocal; import org.springframework.core.PriorityOrdered; -import org.springframework.lang.Nullable; /** * Interceptor that exposes the current {@link org.aopalliance.intercept.MethodInvocation} @@ -89,8 +89,7 @@ private ExposeInvocationInterceptor() { } @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { MethodInvocation oldInvocation = invocation.get(); invocation.set(mi); try { diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java index 610f950cff77..f47c9978756b 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/PerformanceMonitorInterceptor.java @@ -18,6 +18,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.util.StopWatch; @@ -53,7 +54,7 @@ public PerformanceMonitorInterceptor(boolean useDynamicLogger) { @Override - protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + protected @Nullable Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { String name = createInvocationTraceName(invocation); StopWatch stopWatch = new StopWatch(name); stopWatch.start(name); diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java index d11f0d90d821..a9ed90d1cc7f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleAsyncUncaughtExceptionHandler.java @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; /** * A default {@link AsyncUncaughtExceptionHandler} that simply logs the exception. @@ -34,7 +35,7 @@ public class SimpleAsyncUncaughtExceptionHandler implements AsyncUncaughtExcepti @Override - public void handleUncaughtException(Throwable ex, Method method, Object... params) { + public void handleUncaughtException(Throwable ex, Method method, @Nullable Object... params) { if (logger.isErrorEnabled()) { logger.error("Unexpected exception occurred invoking async method: " + method, ex); } diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java index f53fd86ed937..23ff6729650c 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/SimpleTraceInterceptor.java @@ -18,6 +18,7 @@ import org.aopalliance.intercept.MethodInvocation; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.util.Assert; @@ -55,7 +56,7 @@ public SimpleTraceInterceptor(boolean useDynamicLogger) { @Override - protected Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { + protected @Nullable Object invokeUnderTrace(MethodInvocation invocation, Log logger) throws Throwable { String invocationDescription = getInvocationDescription(invocation); writeToLog(logger, "Entering " + invocationDescription); try { diff --git a/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java b/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java index eb2a05f4be05..186d58aa073d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/interceptor/package-info.java @@ -3,9 +3,7 @@ * More specific interceptors can be found in corresponding * functionality packages, like "transaction" and "orm". */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.interceptor; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/package-info.java b/spring-aop/src/main/java/org/springframework/aop/package-info.java index 2b87bce534c7..f2d5c60508fd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/package-info.java @@ -17,9 +17,7 @@ *

Spring AOP can be used programmatically or (preferably) * integrated with the Spring IoC container. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java index af2c7af67aff..ff0adb3eb7c8 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyBeanRegistrationAotProcessor.java @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; @@ -38,7 +39,6 @@ import org.springframework.core.ResolvableType; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; /** * {@link BeanRegistrationAotProcessor} for {@link ScopedProxyFactoryBean}. @@ -53,9 +53,8 @@ class ScopedProxyBeanRegistrationAotProcessor implements BeanRegistrationAotProc @Override - @Nullable - @SuppressWarnings("NullAway") - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + @SuppressWarnings("NullAway") // Lambda + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (beanClass.equals(ScopedProxyFactoryBean.class)) { String targetBeanName = getTargetBeanName(registeredBean.getMergedBeanDefinition()); @@ -73,14 +72,12 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe return null; } - @Nullable - private String getTargetBeanName(BeanDefinition beanDefinition) { + private @Nullable String getTargetBeanName(BeanDefinition beanDefinition) { Object value = beanDefinition.getPropertyValues().get("targetBeanName"); return (value instanceof String targetBeanName ? targetBeanName : null); } - @Nullable - private BeanDefinition getTargetBeanDefinition( + private @Nullable BeanDefinition getTargetBeanDefinition( ConfigurableBeanFactory beanFactory, @Nullable String targetBeanName) { if (targetBeanName != null && beanFactory.containsBean(targetBeanName)) { diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java index a787a1ee809c..8beeff41ff26 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyFactoryBean.java @@ -18,6 +18,8 @@ import java.lang.reflect.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.ProxyConfig; import org.springframework.aop.framework.ProxyFactory; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -59,12 +60,10 @@ public class ScopedProxyFactoryBean extends ProxyConfig private final SimpleBeanTargetSource scopedTargetSource = new SimpleBeanTargetSource(); /** The name of the target bean. */ - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; /** The cached singleton proxy. */ - @Nullable - private Object proxy; + private @Nullable Object proxy; /** @@ -117,8 +116,7 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { if (this.proxy == null) { throw new FactoryBeanNotInitializedException(); } @@ -126,8 +124,7 @@ public Object getObject() { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { if (this.proxy != null) { return this.proxy.getClass(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java index 2eee3a42581e..f2f073613b86 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/ScopedProxyUtils.java @@ -16,6 +16,8 @@ package org.springframework.aop.scope; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -23,7 +25,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java b/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java index 443f903968fb..2736df6ebf72 100644 --- a/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/scope/package-info.java @@ -1,9 +1,7 @@ /** * Support for AOP-based scoping of target objects, with configurable backend. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.scope; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java index e6a10c621bf1..6afa797771c9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractBeanFactoryPointcutAdvisor.java @@ -20,10 +20,10 @@ import java.io.ObjectInputStream; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,14 +42,11 @@ @SuppressWarnings("serial") public abstract class AbstractBeanFactoryPointcutAdvisor extends AbstractPointcutAdvisor implements BeanFactoryAware { - @Nullable - private String adviceBeanName; + private @Nullable String adviceBeanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private transient volatile Advice advice; + private transient volatile @Nullable Advice advice; private transient Object adviceMonitor = new Object(); @@ -69,8 +66,7 @@ public void setAdviceBeanName(@Nullable String adviceBeanName) { /** * Return the name of the advice bean that this advisor refers to, if any. */ - @Nullable - public String getAdviceBeanName() { + public @Nullable String getAdviceBeanName() { return this.adviceBeanName; } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java index 5330f2c00d64..080751196ae9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractExpressionPointcut.java @@ -18,7 +18,7 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract superclass for expression pointcuts, @@ -33,11 +33,9 @@ @SuppressWarnings("serial") public abstract class AbstractExpressionPointcut implements ExpressionPointcut, Serializable { - @Nullable - private String location; + private @Nullable String location; - @Nullable - private String expression; + private @Nullable String expression; /** @@ -53,8 +51,7 @@ public void setLocation(@Nullable String location) { * @return location information as a human-readable String, * or {@code null} if none is available */ - @Nullable - public String getLocation() { + public @Nullable String getLocation() { return this.location; } @@ -89,8 +86,7 @@ protected void onSetExpression(@Nullable String expression) throws IllegalArgume * Return this pointcut's expression. */ @Override - @Nullable - public String getExpression() { + public @Nullable String getExpression() { return this.expression; } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java index fc5527270ed8..72cd6011d1c1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractPointcutAdvisor.java @@ -19,10 +19,10 @@ import java.io.Serializable; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.PointcutAdvisor; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -38,8 +38,7 @@ @SuppressWarnings("serial") public abstract class AbstractPointcutAdvisor implements PointcutAdvisor, Ordered, Serializable { - @Nullable - private Integer order; + private @Nullable Integer order; public void setOrder(int order) { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java index acb56fd468d2..885f70a79cee 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AbstractRegexpMethodPointcut.java @@ -20,7 +20,8 @@ import java.lang.reflect.Method; import java.util.Arrays; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java index 38eba72e6d7a..d27f4b66c32e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/AopUtils.java @@ -28,6 +28,7 @@ import kotlin.coroutines.Continuation; import kotlin.coroutines.CoroutineContext; import kotlinx.coroutines.Job; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Advisor; import org.springframework.aop.AopInvocationException; @@ -43,7 +44,6 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.MethodIntrospector; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -347,8 +347,7 @@ public static List findAdvisorsThatCanApply(List candidateAdvi * @throws Throwable if thrown by the target method * @throws org.springframework.aop.AopInvocationException in case of a reflection error */ - @Nullable - public static Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, Object[] args) + public static @Nullable Object invokeJoinpointUsingReflection(@Nullable Object target, Method method, @Nullable Object[] args) throws Throwable { // Use reflection to invoke the method. @@ -378,7 +377,7 @@ public static Object invokeJoinpointUsingReflection(@Nullable Object target, Met */ private static class KotlinDelegate { - public static Object invokeSuspendingFunction(Method method, @Nullable Object target, Object... args) { + public static Object invokeSuspendingFunction(Method method, @Nullable Object target, @Nullable Object... args) { Continuation continuation = (Continuation) args[args.length -1]; Assert.state(continuation != null, "No Continuation available"); CoroutineContext context = continuation.getContext().minusKey(Job.Key); diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java index 929196e66b74..df7dcfd694d2 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ClassFilters.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,8 +20,9 @@ import java.util.Arrays; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -198,8 +199,8 @@ public boolean matches(Class clazz) { @Override public boolean equals(Object other) { - return (this == other || (other instanceof NegateClassFilter that - && this.original.equals(that.original))); + return (this == other || (other instanceof NegateClassFilter that && + this.original.equals(that.original))); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java index 432635510c4a..40ba09555dc9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ComposablePointcut.java @@ -18,10 +18,11 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java index 43707df5c200..63f1a4358a7a 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ControlFlowPointcut.java @@ -23,10 +23,11 @@ import java.util.List; import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PatternMatchUtils; @@ -143,7 +144,7 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { incrementEvaluationCount(); for (StackTraceElement element : new Throwable().getStackTrace()) { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java index ce68704856c6..80ac2ea2612f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultBeanFactoryPointcutAdvisor.java @@ -16,8 +16,9 @@ package org.springframework.aop.support; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; /** * Concrete BeanFactory-based PointcutAdvisor that allows for any Advice diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java index 930255c62cd6..f95cf95a0b2d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultIntroductionAdvisor.java @@ -21,13 +21,13 @@ import java.util.Set; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.ClassFilter; import org.springframework.aop.DynamicIntroductionAdvice; import org.springframework.aop.IntroductionAdvisor; import org.springframework.aop.IntroductionInfo; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java index 45c2e17254b9..b6e5a5dd80ce 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DefaultPointcutAdvisor.java @@ -19,9 +19,9 @@ import java.io.Serializable; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; /** * Convenient Pointcut-driven Advisor implementation. diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java index 0f3d511c0dea..6a4ebc23ccdd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DelegatePerTargetObjectIntroductionInterceptor.java @@ -20,11 +20,11 @@ import java.util.WeakHashMap; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.DynamicIntroductionAdvice; import org.springframework.aop.IntroductionInterceptor; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -86,8 +86,7 @@ public DelegatePerTargetObjectIntroductionInterceptor(Class defaultImplType, * method, which handles introduced interfaces and forwarding to the target. */ @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { if (isMethodOnIntroducedInterface(mi)) { Object delegate = getIntroductionDelegateFor(mi.getThis()); @@ -114,8 +113,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { * that it is introduced into. This method is never called for * {@link MethodInvocation MethodInvocations} on the introduced interfaces. */ - @Nullable - protected Object doProceed(MethodInvocation mi) throws Throwable { + protected @Nullable Object doProceed(MethodInvocation mi) throws Throwable { // If we get here, just pass the invocation on. return mi.proceed(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java b/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java index bd9647a0f462..1948ef564702 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/DelegatingIntroductionInterceptor.java @@ -17,11 +17,11 @@ package org.springframework.aop.support; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.DynamicIntroductionAdvice; import org.springframework.aop.IntroductionInterceptor; import org.springframework.aop.ProxyMethodInvocation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -57,8 +57,7 @@ public class DelegatingIntroductionInterceptor extends IntroductionInfoSupport * Object that actually implements the interfaces. * May be "this" if a subclass implements the introduced interfaces. */ - @Nullable - private Object delegate; + private @Nullable Object delegate; /** @@ -102,8 +101,7 @@ private void init(Object delegate) { * method, which handles introduced interfaces and forwarding to the target. */ @Override - @Nullable - public Object invoke(MethodInvocation mi) throws Throwable { + public @Nullable Object invoke(MethodInvocation mi) throws Throwable { if (isMethodOnIntroducedInterface(mi)) { // Using the following method rather than direct reflection, we // get correct handling of InvocationTargetException @@ -131,8 +129,7 @@ public Object invoke(MethodInvocation mi) throws Throwable { * that it is introduced into. This method is never called for * {@link MethodInvocation MethodInvocations} on the introduced interfaces. */ - @Nullable - protected Object doProceed(MethodInvocation mi) throws Throwable { + protected @Nullable Object doProceed(MethodInvocation mi) throws Throwable { // If we get here, just pass the invocation on. return mi.proceed(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java index 99b76e135d32..a4ba549c6b2d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/ExpressionPointcut.java @@ -16,8 +16,9 @@ package org.springframework.aop.support; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; /** * Interface to be implemented by pointcuts that use String expressions. @@ -30,7 +31,6 @@ public interface ExpressionPointcut extends Pointcut { /** * Return the String expression for this pointcut. */ - @Nullable - String getExpression(); + @Nullable String getExpression(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java index f2d226adfb28..ed7d584b2332 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/MethodMatchers.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,10 +20,11 @@ import java.lang.reflect.Method; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.IntroductionAwareMethodMatcher; import org.springframework.aop.MethodMatcher; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -150,7 +151,7 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { return this.mm1.matches(method, targetClass, args) || this.mm2.matches(method, targetClass, args); } @@ -302,7 +303,7 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { // Because a dynamic intersection may be composed of a static and dynamic part, // we must avoid calling the 3-arg matches method on a dynamic matcher, as // it will probably be an unsupported operation. @@ -372,14 +373,14 @@ public boolean isRuntime() { } @Override - public boolean matches(Method method, Class targetClass, Object... args) { + public boolean matches(Method method, Class targetClass, @Nullable Object... args) { return !this.original.matches(method, targetClass, args); } @Override public boolean equals(Object other) { - return (this == other || (other instanceof NegateMethodMatcher that - && this.original.equals(that.original))); + return (this == other || (other instanceof NegateMethodMatcher that && + this.original.equals(that.original))); } @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java index 9a11e60b8733..b3967854d321 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/NameMatchMethodPointcut.java @@ -22,7 +22,8 @@ import java.util.Arrays; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.PatternMatchUtils; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java b/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java index bc41a9fbdd8d..79dca1470735 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/RegexpMethodPointcutAdvisor.java @@ -19,9 +19,9 @@ import java.io.Serializable; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -45,11 +45,9 @@ @SuppressWarnings("serial") public class RegexpMethodPointcutAdvisor extends AbstractGenericPointcutAdvisor { - @Nullable - private String[] patterns; + private String @Nullable [] patterns; - @Nullable - private AbstractRegexpMethodPointcut pointcut; + private @Nullable AbstractRegexpMethodPointcut pointcut; private final Object pointcutMonitor = new SerializableMonitor(); diff --git a/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java index c4bf82a18608..d0cf1eb21a6e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/RootClassFilter.java @@ -18,8 +18,9 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java index 482ecfd1c26a..7e11e7d06e54 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/StaticMethodMatcher.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.MethodMatcher; /** @@ -34,7 +36,7 @@ public final boolean isRuntime() { } @Override - public final boolean matches(Method method, Class targetClass, Object... args) { + public final boolean matches(Method method, Class targetClass, @Nullable Object... args) { // should never be invoked because isRuntime() returns false throw new UnsupportedOperationException("Illegal MethodMatcher usage"); } diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java index 9b234633add6..6385d108f7b7 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationClassFilter.java @@ -18,9 +18,10 @@ import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java index c6e11ecf77ae..340e5d25452e 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMatchingPointcut.java @@ -18,11 +18,12 @@ import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.MethodMatcher; import org.springframework.aop.Pointcut; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -87,7 +88,7 @@ public AnnotationMatchingPointcut(@Nullable Class classAnn * @see AnnotationClassFilter#AnnotationClassFilter(Class, boolean) * @see AnnotationMethodMatcher#AnnotationMethodMatcher(Class, boolean) */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public AnnotationMatchingPointcut(@Nullable Class classAnnotationType, @Nullable Class methodAnnotationType, boolean checkInherited) { diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java index 520519ff7243..3e637663f0b5 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/AnnotationMethodMatcher.java @@ -20,10 +20,11 @@ import java.lang.reflect.Method; import java.lang.reflect.Proxy; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.aop.support.StaticMethodMatcher; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java b/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java index a5ec1d421ab5..367eb885438d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotation support for AOP pointcuts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.support.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/support/package-info.java b/spring-aop/src/main/java/org/springframework/aop/support/package-info.java index a39f2d4c302c..af967794b426 100644 --- a/spring-aop/src/main/java/org/springframework/aop/support/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/support/package-info.java @@ -1,9 +1,7 @@ /** * Convenience classes for using Spring's AOP API. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java index 6ab195c0a7bd..ab40e00cce51 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractBeanFactoryBasedTargetSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -60,19 +60,17 @@ public abstract class AbstractBeanFactoryBasedTargetSource implements TargetSour protected final transient Log logger = LogFactory.getLog(getClass()); /** Name of the target bean we will create on each invocation. */ - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; /** Class of the target. */ - @Nullable - private volatile Class targetClass; + private volatile @Nullable Class targetClass; /** * BeanFactory that owns this TargetSource. We need to hold onto this * reference so that we can create new prototype instances as necessary. */ - @Nullable - private BeanFactory beanFactory; + @SuppressWarnings("serial") + private @Nullable BeanFactory beanFactory; /** @@ -128,8 +126,7 @@ public BeanFactory getBeanFactory() { @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { Class targetClass = this.targetClass; if (targetClass != null) { return targetClass; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java index 57838757015d..7c8af147ad8d 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractLazyCreationTargetSource.java @@ -18,9 +18,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; /** * {@link org.springframework.aop.TargetSource} implementation that will @@ -46,8 +46,7 @@ public abstract class AbstractLazyCreationTargetSource implements TargetSource { protected final Log logger = LogFactory.getLog(getClass()); /** The lazily initialized target object. */ - @Nullable - private Object lazyTarget; + private @Nullable Object lazyTarget; /** @@ -67,8 +66,7 @@ public synchronized boolean isInitialized() { * @see #isInitialized() */ @Override - @Nullable - public synchronized Class getTargetClass() { + public synchronized @Nullable Class getTargetClass() { return (this.lazyTarget != null ? this.lazyTarget.getClass() : null); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java index 30e9c1e2ef23..e6cf192e8fa9 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/AbstractPoolingTargetSource.java @@ -16,13 +16,14 @@ package org.springframework.aop.target; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.DefaultIntroductionAdvisor; import org.springframework.aop.support.DelegatingIntroductionInterceptor; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.DisposableBean; -import org.springframework.lang.Nullable; /** * Abstract base class for pooling {@link org.springframework.aop.TargetSource} @@ -101,8 +102,7 @@ public final void setBeanFactory(BeanFactory beanFactory) throws BeansException * APIs, so we're forgiving with our exception signature */ @Override - @Nullable - public abstract Object getTarget() throws Exception; + public abstract @Nullable Object getTarget() throws Exception; /** * Return the given object to the pool. diff --git a/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java index 4c0e5b48da6d..8160f62a4879 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/CommonsPool2TargetSource.java @@ -22,8 +22,8 @@ import org.apache.commons.pool2.impl.DefaultPooledObject; import org.apache.commons.pool2.impl.GenericObjectPool; import org.apache.commons.pool2.impl.GenericObjectPoolConfig; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,8 +81,7 @@ public class CommonsPool2TargetSource extends AbstractPoolingTargetSource implem /** * The Apache Commons {@code ObjectPool} used to pool target objects. */ - @Nullable - private ObjectPool pool; + private @Nullable ObjectPool pool; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java index cfcb3b119fdf..75575446f3cd 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/EmptyTargetSource.java @@ -19,8 +19,9 @@ import java.io.Serializable; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -71,8 +72,7 @@ public static EmptyTargetSource forClass(@Nullable Class targetClass, boolean // Instance implementation //--------------------------------------------------------------------- - @Nullable - private final Class targetClass; + private final @Nullable Class targetClass; private final boolean isStatic; @@ -94,8 +94,7 @@ private EmptyTargetSource(@Nullable Class targetClass, boolean isStatic) { * Always returns the specified target Class, or {@code null} if none. */ @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetClass; } @@ -111,8 +110,7 @@ public boolean isStatic() { * Always returns {@code null}. */ @Override - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { return null; } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java index fb5aceefcadf..dd4babd56c2f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/HotSwappableTargetSource.java @@ -18,8 +18,9 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-aop/src/main/java/org/springframework/aop/target/LazyInitTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/LazyInitTargetSource.java index e69a01842e00..3b766190a773 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/LazyInitTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/LazyInitTargetSource.java @@ -16,8 +16,9 @@ package org.springframework.aop.target; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; /** * {@link org.springframework.aop.TargetSource} that lazily accesses a @@ -60,8 +61,7 @@ @SuppressWarnings("serial") public class LazyInitTargetSource extends AbstractBeanFactoryBasedTargetSource { - @Nullable - private Object target; + private @Nullable Object target; @Override diff --git a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java index 11e3a9e8fead..f947f2980462 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/SingletonTargetSource.java @@ -18,8 +18,9 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java index 5871ac8b95d1..09844bc846c1 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/AbstractRefreshableTargetSource.java @@ -18,9 +18,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; /** * Abstract {@link org.springframework.aop.TargetSource} implementation that @@ -42,7 +42,7 @@ public abstract class AbstractRefreshableTargetSource implements TargetSource, R /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable + @SuppressWarnings("NullAway.Init") protected Object targetObject; private long refreshCheckDelay = -1; @@ -66,7 +66,6 @@ public void setRefreshCheckDelay(long refreshCheckDelay) { @Override - @SuppressWarnings("NullAway") public synchronized Class getTargetClass() { if (this.targetObject == null) { refresh(); @@ -75,8 +74,7 @@ public synchronized Class getTargetClass() { } @Override - @Nullable - public final synchronized Object getTarget() { + public final synchronized @Nullable Object getTarget() { if ((refreshCheckDelayElapsed() && requiresRefresh()) || this.targetObject == null) { refresh(); } diff --git a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java index 5ac4c66c1820..27d8af4ff16f 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/dynamic/package-info.java @@ -2,9 +2,7 @@ * Support for dynamic, refreshable {@link org.springframework.aop.TargetSource} * implementations for use with Spring AOP. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.target.dynamic; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/main/java/org/springframework/aop/target/package-info.java b/spring-aop/src/main/java/org/springframework/aop/target/package-info.java index 292cdcce5d1e..88fb11976c00 100644 --- a/spring-aop/src/main/java/org/springframework/aop/target/package-info.java +++ b/spring-aop/src/main/java/org/springframework/aop/target/package-info.java @@ -2,9 +2,7 @@ * Various {@link org.springframework.aop.TargetSource} implementations for use * with Spring AOP. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aop.target; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java index d7dec7bf33d1..74484dcf7006 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/TrickyAspectJPointcutExpressionTests.java @@ -24,6 +24,7 @@ import java.lang.annotation.Target; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -32,7 +33,6 @@ import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.support.DefaultPointcutAdvisor; import org.springframework.core.OverridingClassLoader; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java index 02d968212d53..03cc27f239f0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AbstractAspectJAdvisorFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -203,7 +203,6 @@ void perThisAspect() throws Exception { itb.getSpouse(); assertThat(maaif.isMaterialized()).isTrue(); - assertThat(imapa.getDeclaredPointcut().getMethodMatcher().matches(TestBean.class.getMethod("getAge"), null)).isTrue(); assertThat(itb.getAge()).as("Around advice must apply").isEqualTo(0); @@ -301,7 +300,7 @@ void bindingWithSingleArg() { void bindingWithMultipleArgsDifferentlyOrdered() { ManyValuedArgs target = new ManyValuedArgs(); ManyValuedArgs mva = createProxy(target, ManyValuedArgs.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new ManyValuedArgs(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new ManyValuedArgs(), "someBean"))); String a = "a"; int b = 12; @@ -320,7 +319,7 @@ void introductionOnTargetNotImplementingInterface() { NotLockable notLockableTarget = new NotLockable(); assertThat(notLockableTarget).isNotInstanceOf(Lockable.class); NotLockable notLockable1 = createProxy(notLockableTarget, NotLockable.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); assertThat(notLockable1).isInstanceOf(Lockable.class); Lockable lockable = (Lockable) notLockable1; assertThat(lockable.locked()).isFalse(); @@ -329,7 +328,7 @@ void introductionOnTargetNotImplementingInterface() { NotLockable notLockable2Target = new NotLockable(); NotLockable notLockable2 = createProxy(notLockable2Target, NotLockable.class, - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean"))); assertThat(notLockable2).isInstanceOf(Lockable.class); Lockable lockable2 = (Lockable) notLockable2; assertThat(lockable2.locked()).isFalse(); @@ -343,20 +342,19 @@ void introductionOnTargetNotImplementingInterface() { void introductionAdvisorExcludedFromTargetImplementingInterface() { assertThat(AopUtils.findAdvisorsThatCanApply( getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(), "someBean")), + aspectInstanceFactory(new MakeLockable(), "someBean")), CannotBeUnlocked.class)).isEmpty(); assertThat(AopUtils.findAdvisorsThatCanApply(getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class)).hasSize(2); + aspectInstanceFactory(new MakeLockable(),"someBean")), NotLockable.class)).hasSize(2); } @Test void introductionOnTargetImplementingInterface() { CannotBeUnlocked target = new CannotBeUnlocked(); Lockable proxy = createProxy(target, CannotBeUnlocked.class, - // Ensure that we exclude AopUtils.findAdvisorsThatCanApply( - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), - CannotBeUnlocked.class)); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), + CannotBeUnlocked.class)); assertThat(proxy).isInstanceOf(Lockable.class); Lockable lockable = proxy; assertThat(lockable.locked()).as("Already locked").isTrue(); @@ -370,8 +368,8 @@ void introductionOnTargetExcludedByTypePattern() { ArrayList target = new ArrayList<>(); List proxy = createProxy(target, List.class, AopUtils.findAdvisorsThatCanApply( - getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), - List.class)); + getAdvisorFactory().getAdvisors(aspectInstanceFactory(new MakeLockable(), "someBean")), + List.class)); assertThat(proxy).as("Type pattern must have excluded mixin").isNotInstanceOf(Lockable.class); } @@ -379,7 +377,7 @@ void introductionOnTargetExcludedByTypePattern() { void introductionBasedOnAnnotationMatch() { // gh-9980 AnnotatedTarget target = new AnnotatedTargetImpl(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); + aspectInstanceFactory(new MakeAnnotatedTypeModifiable(), "someBean")); Object proxy = createProxy(target, AnnotatedTarget.class, advisors); assertThat(proxy).isInstanceOf(Lockable.class); Lockable lockable = (Lockable) proxy; @@ -393,9 +391,9 @@ void introductionWithArgumentBinding() { TestBean target = new TestBean(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); + aspectInstanceFactory(new MakeITestBeanModifiable(), "someBean")); advisors.addAll(getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new MakeLockable(), "someBean"))); + aspectInstanceFactory(new MakeLockable(), "someBean"))); Modifiable modifiable = (Modifiable) createProxy(target, ITestBean.class, advisors); assertThat(modifiable).isInstanceOf(Modifiable.class); @@ -426,7 +424,7 @@ void aspectMethodThrowsExceptionLegalOnSignature() { TestBean target = new TestBean(); UnsupportedOperationException expectedException = new UnsupportedOperationException(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); assertThat(advisors).as("One advice method was found").hasSize(1); ITestBean itb = createProxy(target, ITestBean.class, advisors); assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(itb::getAge); @@ -439,12 +437,12 @@ void aspectMethodThrowsExceptionIllegalOnSignature() { TestBean target = new TestBean(); RemoteException expectedException = new RemoteException(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); + aspectInstanceFactory(new ExceptionThrowingAspect(expectedException), "someBean")); assertThat(advisors).as("One advice method was found").hasSize(1); ITestBean itb = createProxy(target, ITestBean.class, advisors); assertThatExceptionOfType(UndeclaredThrowableException.class) - .isThrownBy(itb::getAge) - .withCause(expectedException); + .isThrownBy(itb::getAge) + .withCause(expectedException); } @Test @@ -452,7 +450,7 @@ void twoAdvicesOnOneAspect() { TestBean target = new TestBean(); TwoAdviceAspect twoAdviceAspect = new TwoAdviceAspect(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(twoAdviceAspect, "someBean")); + aspectInstanceFactory(twoAdviceAspect, "someBean")); assertThat(advisors).as("Two advice methods found").hasSize(2); ITestBean itb = createProxy(target, ITestBean.class, advisors); itb.setName(""); @@ -466,7 +464,7 @@ void twoAdvicesOnOneAspect() { void afterAdviceTypes() throws Exception { InvocationTrackingAspect aspect = new InvocationTrackingAspect(); List advisors = getAdvisorFactory().getAdvisors( - aspectInstanceFactory(aspect, "exceptionHandlingAspect")); + aspectInstanceFactory(aspect, "exceptionHandlingAspect")); Echo echo = createProxy(new Echo(), Echo.class, advisors); assertThat(aspect.invocations).isEmpty(); @@ -475,7 +473,7 @@ void afterAdviceTypes() throws Exception { aspect.invocations.clear(); assertThatExceptionOfType(FileNotFoundException.class) - .isThrownBy(() -> echo.echo(new FileNotFoundException())); + .isThrownBy(() -> echo.echo(new FileNotFoundException())); assertThat(aspect.invocations).containsExactly("around - start", "before", "after throwing", "after", "around - end"); } @@ -487,7 +485,6 @@ void nonAbstractParentAspect() { assertThat(Modifier.isAbstract(aspect.getClass().getSuperclass().getModifiers())).isFalse(); List advisors = getAdvisorFactory().getAdvisors(aspectInstanceFactory(aspect, "incrementingAspect")); - ITestBean proxy = createProxy(new TestBean("Jane", 42), ITestBean.class, advisors); assertThat(proxy.getAge()).isEqualTo(86); // (42 + 1) * 2 } @@ -812,19 +809,19 @@ void before() { invocations.add("before"); } - @AfterReturning("echo()") - void afterReturning() { - invocations.add("after returning"); + @After("echo()") + void after() { + invocations.add("after"); } - @AfterThrowing("echo()") - void afterThrowing() { - invocations.add("after throwing"); + @AfterReturning(pointcut = "this(target) && execution(* echo(*))", returning = "returnValue") + void afterReturning(JoinPoint joinPoint, Echo target, Object returnValue) { + invocations.add("after returning"); } - @After("echo()") - void after() { - invocations.add("after"); + @AfterThrowing(pointcut = "this(target) && execution(* echo(*))", throwing = "exception") + void afterThrowing(JoinPoint joinPoint, Echo target, Throwable exception) { + invocations.add("after throwing"); } } @@ -967,7 +964,7 @@ private Method getGetterFromSetter(Method setter) { class MakeITestBeanModifiable extends AbstractMakeModifiable { @DeclareParents(value = "org.springframework.beans.testfixture.beans.ITestBean+", - defaultImpl=ModifiableImpl.class) + defaultImpl = ModifiableImpl.class) static MutableModifiable mixin; } diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java index 124b1612acd5..446b9bc34956 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJAdvisorBeanRegistrationAotProcessorTests.java @@ -16,6 +16,7 @@ package org.springframework.aop.aspectj.annotation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -48,7 +48,7 @@ class AspectJAdvisorBeanRegistrationAotProcessorTests { @Test void shouldProcessAspectJClass() { process(AspectJClass.class); - assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.DECLARED_FIELDS)) + assertThat(reflection().onType(AspectJClass.class).withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)) .accepts(this.runtimeHints); } @@ -65,8 +65,7 @@ void process(Class beanClass) { } } - @Nullable - private static BeanRegistrationAotContribution createContribution(Class beanClass) { + private static @Nullable BeanRegistrationAotContribution createContribution(Class beanClass) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); return new AspectJAdvisorBeanRegistrationAotProcessor() diff --git a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessorTests.java b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessorTests.java index 810409ee0fda..6ab88d29fb33 100644 --- a/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/aspectj/annotation/AspectJBeanFactoryInitializationAotProcessorTests.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,6 +20,7 @@ import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Pointcut; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; @@ -28,7 +29,6 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -50,7 +50,7 @@ void shouldSkipEmptyClass() { @Test void shouldProcessAspect() { process(TestAspect.class); - assertThat(RuntimeHintsPredicates.reflection().onMethod(TestAspect.class, "alterReturnValue").invoke()) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(TestAspect.class, "alterReturnValue")) .accepts(this.generationContext.getRuntimeHints()); } @@ -61,8 +61,7 @@ private void process(Class beanClass) { } } - @Nullable - private static BeanFactoryInitializationAotContribution createContribution(Class beanClass) { + private static @Nullable BeanFactoryInitializationAotContribution createContribution(Class beanClass) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); return new AspectJBeanFactoryInitializationAotProcessor().processAheadOfTime(beanFactory); 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 6246d32ec36e..bba53793ccc8 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 @@ -20,16 +20,15 @@ import java.util.Objects; import org.aopalliance.intercept.MethodInterceptor; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.IndicativeSentencesGeneration; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.mockito.stubbing.Answer; -import org.springframework.lang.Nullable; - 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; @@ -41,6 +40,7 @@ * @see JdkProxyExceptionHandlingTests * @see CglibProxyExceptionHandlingTests */ +@IndicativeSentencesGeneration(generator = SentenceFragmentDisplayNameGenerator.class) abstract class AbstractProxyExceptionHandlingTests { private static final RuntimeException uncheckedException = new RuntimeException(); @@ -68,7 +68,12 @@ void clear() { private void invokeProxy() { - throwableSeenByCaller = catchThrowable(() -> Objects.requireNonNull(proxy).doSomething()); + try { + Objects.requireNonNull(proxy).doSomething(); + } + catch (Throwable throwable) { + throwableSeenByCaller = throwable; + } } @SuppressWarnings("SameParameterValue") @@ -80,10 +85,10 @@ private static Answer sneakyThrow(Throwable throwable) { @Nested + @SentenceFragment("when there is one interceptor") class WhenThereIsOneInterceptorTests { - @Nullable - private Throwable throwableSeenByInterceptor; + private @Nullable Throwable throwableSeenByInterceptor; @BeforeEach void beforeEach() { @@ -93,6 +98,7 @@ void beforeEach() { } @Test + @SentenceFragment("and the target throws an undeclared checked exception") void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething(); invokeProxy(); @@ -103,6 +109,7 @@ void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { } @Test + @SentenceFragment("and the target throws a declared checked exception") void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { willThrow(declaredCheckedException).given(target).doSomething(); invokeProxy(); @@ -111,6 +118,7 @@ void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { } @Test + @SentenceFragment("and the target throws an unchecked exception") void targetThrowsUncheckedException() throws DeclaredCheckedException { willThrow(uncheckedException).given(target).doSomething(); invokeProxy(); @@ -133,6 +141,7 @@ private MethodInterceptor captureThrowable() { @Nested + @SentenceFragment("when there are no interceptors") class WhenThereAreNoInterceptorsTests { @BeforeEach @@ -142,6 +151,7 @@ void beforeEach() { } @Test + @SentenceFragment("and the target throws an undeclared checked exception") void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { willAnswer(sneakyThrow(undeclaredCheckedException)).given(target).doSomething(); invokeProxy(); @@ -151,6 +161,7 @@ void targetThrowsUndeclaredCheckedException() throws DeclaredCheckedException { } @Test + @SentenceFragment("and the target throws a declared checked exception") void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { willThrow(declaredCheckedException).given(target).doSomething(); invokeProxy(); @@ -158,6 +169,7 @@ void targetThrowsDeclaredCheckedException() throws DeclaredCheckedException { } @Test + @SentenceFragment("and the target throws an unchecked exception") void targetThrowsUncheckedException() throws DeclaredCheckedException { willThrow(uncheckedException).given(target).doSomething(); invokeProxy(); diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/CglibProxyExceptionHandlingTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/CglibProxyExceptionHandlingTests.java index e99075967c3a..2de692498041 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/CglibProxyExceptionHandlingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/CglibProxyExceptionHandlingTests.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.aop.framework; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; import org.springframework.cglib.proxy.Enhancer; @@ -27,6 +28,7 @@ * @since 6.2 * @see JdkProxyExceptionHandlingTests */ +@DisplayName("CGLIB proxy exception handling") class CglibProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests { @BeforeEach diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/JdkProxyExceptionHandlingTests.java b/spring-aop/src/test/java/org/springframework/aop/framework/JdkProxyExceptionHandlingTests.java index a2df44f0e386..a7bae1a7c9e0 100644 --- a/spring-aop/src/test/java/org/springframework/aop/framework/JdkProxyExceptionHandlingTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/framework/JdkProxyExceptionHandlingTests.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,6 +18,8 @@ import java.lang.reflect.Proxy; +import org.junit.jupiter.api.DisplayName; + import static org.assertj.core.api.Assertions.assertThat; /** @@ -25,6 +27,7 @@ * @since 6.2 * @see CglibProxyExceptionHandlingTests */ +@DisplayName("JDK proxy exception handling") class JdkProxyExceptionHandlingTests extends AbstractProxyExceptionHandlingTests { @Override diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragment.java b/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragment.java new file mode 100644 index 000000000000..faf872ca6066 --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragment.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.framework; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * {@code @SentenceFragment} is used to configure a sentence fragment for use + * with JUnit Jupiter's + * {@link org.junit.jupiter.api.DisplayNameGenerator.IndicativeSentences} + * {@code DisplayNameGenerator}. + * + * @author Sam Brannen + * @since 7.0 + * @see SentenceFragmentDisplayNameGenerator + * @see org.junit.jupiter.api.DisplayName + */ +@Target({ ElementType.TYPE, ElementType.METHOD }) +@Retention(RetentionPolicy.RUNTIME) +@interface SentenceFragment { + + String value(); + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragmentDisplayNameGenerator.java b/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragmentDisplayNameGenerator.java new file mode 100644 index 000000000000..ca092fa4bcab --- /dev/null +++ b/spring-aop/src/test/java/org/springframework/aop/framework/SentenceFragmentDisplayNameGenerator.java @@ -0,0 +1,76 @@ +/* + * Copyright 2002-2025 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.aop.framework; + +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.Method; +import java.util.List; + +import org.junit.platform.commons.support.AnnotationSupport; +import org.junit.platform.commons.util.StringUtils; + +/** + * Extension of {@link org.junit.jupiter.api.DisplayNameGenerator.Simple} that + * supports custom sentence fragments configured via + * {@link SentenceFragment @SentenceFragment}. + * + *

This generator can be configured for use with JUnit Jupiter's + * {@link org.junit.jupiter.api.DisplayNameGenerator.IndicativeSentences + * IndicativeSentences} {@code DisplayNameGenerator} via the + * {@link org.junit.jupiter.api.IndicativeSentencesGeneration#generator generator} + * attribute in {@code @IndicativeSentencesGeneration}. + * + * @author Sam Brannen + * @since 7.0 + * @see SentenceFragment @SentenceFragment + */ +class SentenceFragmentDisplayNameGenerator extends org.junit.jupiter.api.DisplayNameGenerator.Simple { + + @Override + public String generateDisplayNameForClass(Class testClass) { + String sentenceFragment = getSentenceFragment(testClass); + return (sentenceFragment != null ? sentenceFragment : + super.generateDisplayNameForClass(testClass)); + } + + @Override + public String generateDisplayNameForNestedClass(List> enclosingInstanceTypes, + Class nestedClass) { + + String sentenceFragment = getSentenceFragment(nestedClass); + return (sentenceFragment != null ? sentenceFragment : + super.generateDisplayNameForNestedClass(enclosingInstanceTypes, nestedClass)); + } + + @Override + public String generateDisplayNameForMethod(List> enclosingInstanceTypes, + Class testClass, Method testMethod) { + + String sentenceFragment = getSentenceFragment(testMethod); + return (sentenceFragment != null ? sentenceFragment : + super.generateDisplayNameForMethod(enclosingInstanceTypes, testClass, testMethod)); + } + + private static final String getSentenceFragment(AnnotatedElement element) { + return AnnotationSupport.findAnnotation(element, SentenceFragment.class) + .map(SentenceFragment::value) + .map(String::trim) + .filter(StringUtils::isNotBlank) + .orElse(null); + } + +} diff --git a/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java b/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java index e18f4d24b8c6..92c295a59ecb 100644 --- a/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/interceptor/AsyncExecutionInterceptorTests.java @@ -27,13 +27,12 @@ import org.springframework.core.task.AsyncTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; - /** * Tests for {@link AsyncExecutionInterceptor}. * diff --git a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java index fb4d1ed6f717..5da2f556be4e 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/AopUtilsTests.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; @@ -31,7 +32,6 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.ResolvableType; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java b/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java index 54b3657703f1..0e59d7530caf 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/ComposablePointcutTests.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; @@ -25,7 +26,6 @@ import org.springframework.aop.Pointcut; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java index 6fae987c9ea7..e1bae0239bfe 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/MethodMatchersTests.java @@ -18,6 +18,7 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.MethodMatcher; @@ -25,7 +26,6 @@ import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; diff --git a/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java index 6f50a9aecc17..cec4198aee10 100644 --- a/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java +++ b/spring-aop/src/test/java/org/springframework/aop/support/PointcutsTests.java @@ -18,12 +18,12 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; import org.springframework.aop.Pointcut; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; 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 ed5ba5ffc9b2..4551a8f3f13d 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 @@ -21,7 +21,7 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract superclass for counting advices etc. diff --git a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java index df7bb2bd98a1..4eaef005af84 100644 --- a/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java +++ b/spring-aop/src/testFixtures/java/org/springframework/aop/testfixture/interceptor/NopInterceptor.java @@ -18,8 +18,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Trivial interceptor that can be introduced in a chain to display it. diff --git a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/package-info.java b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/package-info.java index 675ca10d6886..0497dbf9dbff 100644 --- a/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/package-info.java +++ b/spring-aspects/src/main/java/org/springframework/beans/factory/aspectj/package-info.java @@ -1,9 +1,7 @@ /** * AspectJ-based dependency injection support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.aspectj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/package-info.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/package-info.java index 36080e068da9..b70a6b334672 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/package-info.java +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/package-info.java @@ -1,9 +1,7 @@ /** * AspectJ-based caching support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.aspectj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/package-info.java b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/package-info.java index 4554b676e4bf..aebcc1a57bd8 100644 --- a/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/package-info.java +++ b/spring-aspects/src/main/java/org/springframework/context/annotation/aspectj/package-info.java @@ -3,9 +3,7 @@ * {@link org.springframework.beans.factory.annotation.Configurable @Configurable} * annotation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.annotation.aspectj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/package-info.java b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/package-info.java index 5543ab52fa10..2b1d21941dcb 100644 --- a/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/package-info.java +++ b/spring-aspects/src/main/java/org/springframework/scheduling/aspectj/package-info.java @@ -1,9 +1,7 @@ /** * AspectJ-based scheduling support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling.aspectj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/package-info.java b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/package-info.java index 8b4c08397d73..3b9f9f2da0fe 100644 --- a/spring-aspects/src/main/java/org/springframework/transaction/aspectj/package-info.java +++ b/spring-aspects/src/main/java/org/springframework/transaction/aspectj/package-info.java @@ -1,9 +1,7 @@ /** * AspectJ-based transaction management support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.transaction.aspectj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java index bbc972dbc078..19d1a66c7d7e 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJEnableCachingIsolatedTests.java @@ -95,9 +95,10 @@ void multipleCacheManagerBeans() { } catch (NoUniqueBeanDefinitionException ex) { assertThat(ex.getMessage()).contains( - "no CacheResolver specified and expected single matching CacheManager but found 2: cm1,cm2"); + "no CacheResolver specified and expected single matching CacheManager but found 2") + .contains("cm1", "cm2"); assertThat(ex.getNumberOfBeansFound()).isEqualTo(2); - assertThat(ex.getBeanNamesFound()).containsExactly("cm1", "cm2"); + assertThat(ex.getBeanNamesFound()).containsExactlyInAnyOrder("cm1", "cm2"); } } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java index 0219086ed48d..c069cf4a36fe 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java @@ -18,7 +18,8 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** diff --git a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java index 6cf02e31db2c..3b6877b2411d 100644 --- a/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java +++ b/spring-aspects/src/test/java/org/springframework/scheduling/aspectj/AnnotationAsyncExecutionAspectTests.java @@ -35,7 +35,6 @@ import org.springframework.scheduling.annotation.AsyncResult; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.ReflectionUtils; -import org.springframework.util.concurrent.ListenableFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; @@ -136,10 +135,7 @@ void qualifiedAsyncMethodsAreRoutedToCorrectExecutor() throws InterruptedExcepti assertThat(defaultThread.get()).isNotEqualTo(Thread.currentThread()); assertThat(defaultThread.get().getName()).doesNotStartWith("e1-"); - ListenableFuture e1Thread = obj.e1Work(); - assertThat(e1Thread.get().getName()).startsWith("e1-"); - - CompletableFuture e1OtherThread = obj.e1OtherWork(); + CompletableFuture e1OtherThread = obj.e1Work(); assertThat(e1OtherThread.get().getName()).startsWith("e1-"); } @@ -269,12 +265,7 @@ public Future defaultWork() { } @Async("e1") - public ListenableFuture e1Work() { - return new AsyncResult<>(Thread.currentThread()); - } - - @Async("e1") - public CompletableFuture e1OtherWork() { + public CompletableFuture e1Work() { return CompletableFuture.completedFuture(Thread.currentThread()); } } diff --git a/spring-beans/spring-beans.gradle b/spring-beans/spring-beans.gradle index c4fb10eb3200..a725741630b2 100644 --- a/spring-beans/spring-beans.gradle +++ b/spring-beans/spring-beans.gradle @@ -11,10 +11,8 @@ dependencies { optional("org.reactivestreams:reactive-streams") optional("org.yaml:snakeyaml") testFixturesApi("org.junit.jupiter:junit-jupiter-api") - testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("org.assertj:assertj-core") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-core"))) testImplementation("jakarta.annotation:jakarta.annotation-api") - testImplementation("javax.inject:javax.inject") } diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java index 4c8dcc48dd0c..ee3e805ddea7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractNestablePropertyAccessor.java @@ -33,13 +33,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -78,17 +78,14 @@ public abstract class AbstractNestablePropertyAccessor extends AbstractPropertyA private int autoGrowCollectionLimit = Integer.MAX_VALUE; - @Nullable - Object wrappedObject; + @Nullable Object wrappedObject; private String nestedPath = ""; - @Nullable - Object rootObject; + @Nullable Object rootObject; /** Map with cached nested Accessors: nested path -> Accessor instance. */ - @Nullable - private Map nestedPropertyAccessors; + private @Nullable Map nestedPropertyAccessors; /** @@ -291,7 +288,7 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) String lastKey = tokens.keys[tokens.keys.length - 1]; if (propValue.getClass().isArray()) { - Class requiredType = propValue.getClass().componentType(); + Class componentType = propValue.getClass().componentType(); int arrayIndex = Integer.parseInt(lastKey); Object oldValue = null; try { @@ -299,10 +296,9 @@ private void processKeyedProperty(PropertyTokenHolder tokens, PropertyValue pv) oldValue = Array.get(propValue, arrayIndex); } Object convertedValue = convertIfNecessary(tokens.canonicalName, oldValue, pv.getValue(), - requiredType, ph.nested(tokens.keys.length)); + componentType, ph.nested(tokens.keys.length)); int length = Array.getLength(propValue); if (arrayIndex >= length && arrayIndex < this.autoGrowCollectionLimit) { - Class componentType = propValue.getClass().componentType(); Object newArray = Array.newInstance(componentType, arrayIndex + 1); System.arraycopy(propValue, 0, newArray, 0, length); int lastKeyIndex = tokens.canonicalName.lastIndexOf('['); @@ -488,8 +484,7 @@ private void processLocalProperty(PropertyTokenHolder tokens, PropertyValue pv) } @Override - @Nullable - public Class getPropertyType(String propertyName) throws BeansException { + public @Nullable Class getPropertyType(String propertyName) throws BeansException { try { PropertyHandler ph = getPropertyHandler(propertyName); if (ph != null) { @@ -516,8 +511,7 @@ public Class getPropertyType(String propertyName) throws BeansException { } @Override - @Nullable - public TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { + public @Nullable TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException { try { AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); String finalPath = getFinalPath(nestedPa, propertyName); @@ -580,8 +574,7 @@ public boolean isWritableProperty(String propertyName) { return false; } - @Nullable - private Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, + private @Nullable Object convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable TypeDescriptor td) throws TypeMismatchException { @@ -601,8 +594,7 @@ private Object convertIfNecessary(@Nullable String propertyName, @Nullable Objec } } - @Nullable - protected Object convertForProperty( + protected @Nullable Object convertForProperty( String propertyName, @Nullable Object oldValue, @Nullable Object newValue, TypeDescriptor td) throws TypeMismatchException { @@ -610,16 +602,14 @@ protected Object convertForProperty( } @Override - @Nullable - public Object getPropertyValue(String propertyName) throws BeansException { + public @Nullable Object getPropertyValue(String propertyName) throws BeansException { AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); PropertyTokenHolder tokens = getPropertyNameTokens(getFinalPath(nestedPa, propertyName)); return nestedPa.getPropertyValue(tokens); } @SuppressWarnings({"rawtypes", "unchecked"}) - @Nullable - protected Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { + protected @Nullable Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException { String propertyName = tokens.canonicalName; String actualName = tokens.actualName; PropertyHandler ph = getLocalPropertyHandler(actualName); @@ -734,8 +724,7 @@ else if (value instanceof Iterable iterable) { * or {@code null} if not found * @throws BeansException in case of introspection failure */ - @Nullable - protected PropertyHandler getPropertyHandler(String propertyName) throws BeansException { + protected @Nullable PropertyHandler getPropertyHandler(String propertyName) throws BeansException { Assert.notNull(propertyName, "Property name must not be null"); AbstractNestablePropertyAccessor nestedPa = getPropertyAccessorForPropertyPath(propertyName); return nestedPa.getLocalPropertyHandler(getFinalPath(nestedPa, propertyName)); @@ -747,8 +736,7 @@ protected PropertyHandler getPropertyHandler(String propertyName) throws BeansEx * @param propertyName the name of a local property * @return the handler for that property, or {@code null} if it has not been found */ - @Nullable - protected abstract PropertyHandler getLocalPropertyHandler(String propertyName); + protected abstract @Nullable PropertyHandler getLocalPropertyHandler(String propertyName); /** * Create a new nested property accessor instance. @@ -1025,8 +1013,7 @@ public String toString() { */ protected abstract static class PropertyHandler { - @Nullable - private final Class propertyType; + private final @Nullable Class propertyType; private final boolean readable; @@ -1038,8 +1025,7 @@ public PropertyHandler(@Nullable Class propertyType, boolean readable, boolea this.writable = writable; } - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { return this.propertyType; } @@ -1067,11 +1053,9 @@ public TypeDescriptor getCollectionType(int nestingLevel) { return TypeDescriptor.valueOf(getResolvableType().getNested(nestingLevel).asCollection().resolveGeneric()); } - @Nullable - public abstract TypeDescriptor nested(int level); + public abstract @Nullable TypeDescriptor nested(int level); - @Nullable - public abstract Object getValue() throws Exception; + public abstract @Nullable Object getValue() throws Exception; public abstract void setValue(@Nullable Object value) throws Exception; @@ -1095,8 +1079,7 @@ public PropertyTokenHolder(String name) { public String canonicalName; - @Nullable - public String[] keys; + public String @Nullable [] keys; } } diff --git a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java index 01e67dbdf14d..e25bd96e0b23 100644 --- a/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/AbstractPropertyAccessor.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract implementation of the {@link PropertyAccessor} interface. @@ -139,8 +139,7 @@ public void setPropertyValues(PropertyValues pvs, boolean ignoreUnknown, boolean // Redefined with public visibility. @Override - @Nullable - public Class getPropertyType(String propertyPath) { + public @Nullable Class getPropertyType(String propertyPath) { return null; } @@ -154,8 +153,7 @@ public Class getPropertyType(String propertyPath) { * accessor method failed */ @Override - @Nullable - public abstract Object getPropertyValue(String propertyName) throws BeansException; + public abstract @Nullable Object getPropertyValue(String propertyName) throws BeansException; /** * Actually set a property value. diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java index 395035467132..26958d37eef3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInfoFactory.java @@ -19,7 +19,7 @@ import java.beans.BeanInfo; import java.beans.IntrospectionException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface for creating {@link BeanInfo} instances for Spring beans. @@ -54,7 +54,6 @@ public interface BeanInfoFactory { * @return the BeanInfo, or {@code null} if the given class is not supported * @throws IntrospectionException in case of exceptions */ - @Nullable - BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException; + @Nullable BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException; } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java index a07cae6d3b84..c7bd4d98727f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanInstantiationException.java @@ -19,7 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception thrown when instantiation of a bean failed. @@ -33,11 +33,9 @@ public class BeanInstantiationException extends FatalBeanException { private final Class beanClass; - @Nullable - private final Constructor constructor; + private final @Nullable Constructor constructor; - @Nullable - private final Method constructingMethod; + private final @Nullable Method constructingMethod; /** @@ -106,8 +104,7 @@ public Class getBeanClass() { * factory method or in case of default instantiation * @since 4.3 */ - @Nullable - public Constructor getConstructor() { + public @Nullable Constructor getConstructor() { return this.constructor; } @@ -117,8 +114,7 @@ public Constructor getConstructor() { * or {@code null} in case of constructor-based instantiation * @since 4.3 */ - @Nullable - public Method getConstructingMethod() { + public @Nullable Method getConstructingMethod() { return this.constructingMethod; } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java index b8d316db91ee..8c4ad37bc680 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttribute.java @@ -16,7 +16,8 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -32,11 +33,9 @@ public class BeanMetadataAttribute implements BeanMetadataElement { private final String name; - @Nullable - private final Object value; + private final @Nullable Object value; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -61,8 +60,7 @@ public String getName() { /** * Return the value of the attribute. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } @@ -75,8 +73,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java index 58409cb852da..9aacda4501bf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataAttributeAccessor.java @@ -16,8 +16,9 @@ package org.springframework.beans; +import org.jspecify.annotations.Nullable; + import org.springframework.core.AttributeAccessorSupport; -import org.springframework.lang.Nullable; /** * Extension of {@link org.springframework.core.AttributeAccessorSupport}, @@ -30,8 +31,7 @@ @SuppressWarnings("serial") public class BeanMetadataAttributeAccessor extends AttributeAccessorSupport implements BeanMetadataElement { - @Nullable - private Object source; + private @Nullable Object source; /** @@ -43,8 +43,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -63,8 +62,7 @@ public void addMetadataAttribute(BeanMetadataAttribute attribute) { * @return the corresponding BeanMetadataAttribute object, * or {@code null} if no such attribute defined */ - @Nullable - public BeanMetadataAttribute getMetadataAttribute(String name) { + public @Nullable BeanMetadataAttribute getMetadataAttribute(String name) { return (BeanMetadataAttribute) super.getAttribute(name); } @@ -74,15 +72,13 @@ public void setAttribute(String name, @Nullable Object value) { } @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { BeanMetadataAttribute attribute = (BeanMetadataAttribute) super.getAttribute(name); return (attribute != null ? attribute.getValue() : null); } @Override - @Nullable - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { BeanMetadataAttribute attribute = (BeanMetadataAttribute) super.removeAttribute(name); return (attribute != null ? attribute.getValue() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java index 7126c64ef279..02fe45a4f17f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanMetadataElement.java @@ -16,7 +16,7 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to be implemented by bean metadata elements @@ -31,8 +31,7 @@ public interface BeanMetadataElement { * Return the configuration source {@code Object} for this metadata element * (may be {@code null}). */ - @Nullable - default Object getSource() { + default @Nullable Object getSource() { return null; } 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 f4ca77b3e1ca..984218b36a31 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtils.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,19 +33,20 @@ 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; import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.KCallablesJvm; import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -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()) { @@ -182,11 +182,11 @@ public static T instantiateClass(Class clazz, Class assignableTo) thro * @throws BeanInstantiationException if the bean cannot be instantiated * @see Constructor#newInstance */ - public static T instantiateClass(Constructor ctor, Object... args) throws BeanInstantiationException { + public static T instantiateClass(Constructor ctor, @Nullable Object... args) throws BeanInstantiationException { Assert.notNull(ctor, "Constructor must not be null"); try { ReflectionUtils.makeAccessible(ctor); - if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { + if (KotlinDetector.isKotlinType(ctor.getDeclaringClass())) { return KotlinDelegate.instantiateClass(ctor, args); } else { @@ -196,7 +196,7 @@ public static T instantiateClass(Constructor ctor, Object... args) throws return ctor.newInstance(); } Class[] parameterTypes = ctor.getParameterTypes(); - Object[] argsWithDefaultValues = new Object[args.length]; + @Nullable Object[] argsWithDefaultValues = new Object[args.length]; for (int i = 0 ; i < args.length; i++) { if (args[i] == null) { Class parameterType = parameterTypes[i]; @@ -277,10 +277,9 @@ else if (ctors.length == 0) { * @see Kotlin constructors * @see Record constructor declarations */ - @Nullable - public static Constructor findPrimaryConstructor(Class clazz) { + public static @Nullable Constructor findPrimaryConstructor(Class clazz) { Assert.notNull(clazz, "Class must not be null"); - if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(clazz)) { + if (KotlinDetector.isKotlinType(clazz)) { return KotlinDelegate.findPrimaryConstructor(clazz); } if (clazz.isRecord()) { @@ -313,8 +312,7 @@ public static Constructor findPrimaryConstructor(Class clazz) { * @see Class#getMethod * @see #findDeclaredMethod */ - @Nullable - public static Method findMethod(Class clazz, String methodName, Class... paramTypes) { + public static @Nullable Method findMethod(Class clazz, String methodName, Class... paramTypes) { try { return clazz.getMethod(methodName, paramTypes); } @@ -334,8 +332,7 @@ public static Method findMethod(Class clazz, String methodName, Class... p * @return the Method object, or {@code null} if not found * @see Class#getDeclaredMethod */ - @Nullable - public static Method findDeclaredMethod(Class clazz, String methodName, Class... paramTypes) { + public static @Nullable Method findDeclaredMethod(Class clazz, String methodName, Class... paramTypes) { try { return clazz.getDeclaredMethod(methodName, paramTypes); } @@ -362,8 +359,7 @@ public static Method findDeclaredMethod(Class clazz, String methodName, Class * @see Class#getMethods * @see #findDeclaredMethodWithMinimalParameters */ - @Nullable - public static Method findMethodWithMinimalParameters(Class clazz, String methodName) + public static @Nullable Method findMethodWithMinimalParameters(Class clazz, String methodName) throws IllegalArgumentException { Method targetMethod = findMethodWithMinimalParameters(clazz.getMethods(), methodName); @@ -385,8 +381,7 @@ public static Method findMethodWithMinimalParameters(Class clazz, String meth * could not be resolved to a unique method with minimal parameters * @see Class#getDeclaredMethods */ - @Nullable - public static Method findDeclaredMethodWithMinimalParameters(Class clazz, String methodName) + public static @Nullable Method findDeclaredMethodWithMinimalParameters(Class clazz, String methodName) throws IllegalArgumentException { Method targetMethod = findMethodWithMinimalParameters(clazz.getDeclaredMethods(), methodName); @@ -405,8 +400,7 @@ public static Method findDeclaredMethodWithMinimalParameters(Class clazz, Str * @throws IllegalArgumentException if methods of the given name were found but * could not be resolved to a unique method with minimal parameters */ - @Nullable - public static Method findMethodWithMinimalParameters(Method[] methods, String methodName) + public static @Nullable Method findMethodWithMinimalParameters(Method[] methods, String methodName) throws IllegalArgumentException { Method targetMethod = null; @@ -457,8 +451,7 @@ else if (!method.isBridge() && targetMethod.getParameterCount() == numParams) { * @see #findMethod * @see #findMethodWithMinimalParameters */ - @Nullable - public static Method resolveSignature(String signature, Class clazz) { + public static @Nullable Method resolveSignature(String signature, Class clazz) { Assert.hasText(signature, "'signature' must not be empty"); Assert.notNull(clazz, "Class must not be null"); int startParen = signature.indexOf('('); @@ -511,8 +504,7 @@ public static PropertyDescriptor[] getPropertyDescriptors(Class clazz) throws * @return the corresponding PropertyDescriptor, or {@code null} if none * @throws BeansException if PropertyDescriptor lookup fails */ - @Nullable - public static PropertyDescriptor getPropertyDescriptor(Class clazz, String propertyName) throws BeansException { + public static @Nullable PropertyDescriptor getPropertyDescriptor(Class clazz, String propertyName) throws BeansException { return CachedIntrospectionResults.forClass(clazz).getPropertyDescriptor(propertyName); } @@ -525,8 +517,7 @@ public static PropertyDescriptor getPropertyDescriptor(Class clazz, String pr * @return the corresponding PropertyDescriptor, or {@code null} if none * @throws BeansException if PropertyDescriptor lookup fails */ - @Nullable - public static PropertyDescriptor findPropertyForMethod(Method method) throws BeansException { + public static @Nullable PropertyDescriptor findPropertyForMethod(Method method) throws BeansException { return findPropertyForMethod(method, method.getDeclaringClass()); } @@ -540,8 +531,7 @@ public static PropertyDescriptor findPropertyForMethod(Method method) throws Bea * @throws BeansException if PropertyDescriptor lookup fails * @since 3.2.13 */ - @Nullable - public static PropertyDescriptor findPropertyForMethod(Method method, Class clazz) throws BeansException { + public static @Nullable PropertyDescriptor findPropertyForMethod(Method method, Class clazz) throws BeansException { Assert.notNull(method, "Method must not be null"); PropertyDescriptor[] pds = getPropertyDescriptors(clazz); for (PropertyDescriptor pd : pds) { @@ -561,8 +551,7 @@ public static PropertyDescriptor findPropertyForMethod(Method method, Class c * @param targetType the type to find an editor for * @return the corresponding editor, or {@code null} if none found */ - @Nullable - public static PropertyEditor findEditorByConvention(@Nullable Class targetType) { + public static @Nullable PropertyEditor findEditorByConvention(@Nullable Class targetType) { if (targetType == null || targetType.isArray() || unknownEditorTypes.contains(targetType)) { return null; } @@ -609,7 +598,7 @@ public static PropertyEditor findEditorByConvention(@Nullable Class targetTyp * @param beanClasses the classes to check against * @return the property type, or {@code Object.class} as fallback */ - public static Class findPropertyType(String propertyName, @Nullable Class... beanClasses) { + public static Class findPropertyType(String propertyName, Class @Nullable ... beanClasses) { if (beanClasses != null) { for (Class beanClass : beanClasses) { PropertyDescriptor pd = getPropertyDescriptor(beanClass, propertyName); @@ -665,11 +654,14 @@ public static MethodParameter getWriteMethodParameter(PropertyDescriptor pd) { * @see ConstructorProperties * @see DefaultParameterNameDiscoverer */ - public static String[] getParameterNames(Constructor ctor) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public static @Nullable String[] getParameterNames(Constructor ctor) { ConstructorProperties cp = ctor.getAnnotation(ConstructorProperties.class); - String[] paramNames = (cp != null ? cp.value() : parameterNameDiscoverer.getParameterNames(ctor)); + @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; } @@ -803,7 +795,7 @@ public static void copyProperties(Object source, Object target, String... ignore * @see BeanWrapper */ private static void copyProperties(Object source, Object target, @Nullable Class editable, - @Nullable String... ignoreProperties) throws BeansException { + String @Nullable ... ignoreProperties) throws BeansException { Assert.notNull(source, "Source must not be null"); Assert.notNull(target, "Target must not be null"); @@ -880,8 +872,7 @@ private static class KotlinDelegate { * https://kotlinlang.org/docs/reference/classes.html#constructors */ @SuppressWarnings("unchecked") - @Nullable - public static Constructor findPrimaryConstructor(Class clazz) { + public static @Nullable Constructor findPrimaryConstructor(Class clazz) { try { KClass kClass = JvmClassMappingKt.getKotlinClass(clazz); KFunction primaryCtor = KClasses.getPrimaryConstructor(kClass); @@ -912,7 +903,7 @@ public static Constructor findPrimaryConstructor(Class clazz) { * @param args the constructor arguments to apply * (use {@code null} for unspecified parameter if needed) */ - public static T instantiateClass(Constructor ctor, Object... args) + public static T instantiateClass(Constructor ctor, @Nullable Object... args) throws IllegalAccessException, InvocationTargetException, InstantiationException { KFunction kotlinConstructor = ReflectJvmMapping.getKotlinFunction(ctor); @@ -939,6 +930,11 @@ public static T instantiateClass(Constructor ctor, Object... args) } 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/BeanUtilsRuntimeHints.java b/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java index 3fc2e306739a..721d92216d78 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanUtilsRuntimeHints.java @@ -16,12 +16,13 @@ package org.springframework.beans; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.core.io.ResourceEditor; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} to register hints for popular conventions in diff --git a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java index 93a9724d4420..7171d785214d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeanWrapperImpl.java @@ -20,11 +20,11 @@ import java.lang.reflect.Method; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -64,8 +64,7 @@ public class BeanWrapperImpl extends AbstractNestablePropertyAccessor implements * Cached introspections results for this object, to prevent encountering * the cost of JavaBeans introspection every time. */ - @Nullable - private CachedIntrospectionResults cachedIntrospectionResults; + private @Nullable CachedIntrospectionResults cachedIntrospectionResults; /** @@ -178,8 +177,7 @@ private CachedIntrospectionResults getCachedIntrospectionResults() { * @return the new value, possibly the result of type conversion * @throws TypeMismatchException if type conversion failed */ - @Nullable - public Object convertForProperty(@Nullable Object value, String propertyName) throws TypeMismatchException { + public @Nullable Object convertForProperty(@Nullable Object value, String propertyName) throws TypeMismatchException { CachedIntrospectionResults cachedIntrospectionResults = getCachedIntrospectionResults(); PropertyDescriptor pd = cachedIntrospectionResults.getPropertyDescriptor(propertyName); if (pd == null) { @@ -191,8 +189,7 @@ public Object convertForProperty(@Nullable Object value, String propertyName) th } @Override - @Nullable - protected BeanPropertyHandler getLocalPropertyHandler(String propertyName) { + protected @Nullable BeanPropertyHandler getLocalPropertyHandler(String propertyName) { PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(propertyName); return (pd != null ? new BeanPropertyHandler((GenericTypeAwarePropertyDescriptor) pd) : null); } @@ -261,14 +258,12 @@ public TypeDescriptor getCollectionType(int nestingLevel) { } @Override - @Nullable - public TypeDescriptor nested(int level) { + public @Nullable TypeDescriptor nested(int level) { return this.pd.getTypeDescriptor().nested(level); } @Override - @Nullable - public Object getValue() throws Exception { + public @Nullable Object getValue() throws Exception { Method readMethod = this.pd.getReadMethod(); Assert.state(readMethod != null, "No read method available"); ReflectionUtils.makeAccessible(readMethod); diff --git a/spring-beans/src/main/java/org/springframework/beans/BeansException.java b/spring-beans/src/main/java/org/springframework/beans/BeansException.java index f3816a16db50..fcb8ab9a25f8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/BeansException.java +++ b/spring-beans/src/main/java/org/springframework/beans/BeansException.java @@ -16,8 +16,9 @@ package org.springframework.beans; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * Abstract superclass for all exceptions thrown in the beans package diff --git a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java index f29a9d30407d..6c9014af84a4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java +++ b/spring-beans/src/main/java/org/springframework/beans/CachedIntrospectionResults.java @@ -33,9 +33,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.support.SpringFactoriesLoader; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.StringUtils; @@ -375,8 +375,7 @@ Class getBeanClass() { return this.beanInfo.getBeanDescriptor().getBeanClass(); } - @Nullable - PropertyDescriptor getPropertyDescriptor(String name) { + @Nullable PropertyDescriptor getPropertyDescriptor(String name) { PropertyDescriptor pd = this.propertyDescriptors.get(name); if (pd == null && StringUtils.hasLength(name)) { // Same lenient fallback checking as in Property... diff --git a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java index 38c5d26a4573..9bff02f0d318 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/ConfigurablePropertyAccessor.java @@ -16,8 +16,9 @@ package org.springframework.beans; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; /** * Interface that encapsulates configuration methods for a PropertyAccessor. @@ -42,8 +43,7 @@ public interface ConfigurablePropertyAccessor extends PropertyAccessor, Property /** * Return the associated ConversionService, if any. */ - @Nullable - ConversionService getConversionService(); + @Nullable ConversionService getConversionService(); /** * Set whether to extract the old property value when applying a diff --git a/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java b/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java index 41c7f95a9626..aeb0b4744c48 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java +++ b/spring-beans/src/main/java/org/springframework/beans/ConversionNotSupportedException.java @@ -18,7 +18,7 @@ import java.beans.PropertyChangeEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception thrown when no suitable editor or converter can be found for a bean property. diff --git a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java index 145a5cff9919..9c26ee15ab31 100644 --- a/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/DirectFieldAccessor.java @@ -20,9 +20,10 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -71,8 +72,7 @@ protected DirectFieldAccessor(Object object, String nestedPath, DirectFieldAcces @Override - @Nullable - protected FieldPropertyHandler getLocalPropertyHandler(String propertyName) { + protected @Nullable FieldPropertyHandler getLocalPropertyHandler(String propertyName) { FieldPropertyHandler propertyHandler = this.fieldMap.get(propertyName); if (propertyHandler == null) { Field field = ReflectionUtils.findField(getWrappedClass(), propertyName); @@ -132,14 +132,12 @@ public TypeDescriptor getCollectionType(int nestingLevel) { } @Override - @Nullable - public TypeDescriptor nested(int level) { + public @Nullable TypeDescriptor nested(int level) { return TypeDescriptor.nested(this.field, level); } @Override - @Nullable - public Object getValue() throws Exception { + public @Nullable Object getValue() throws Exception { try { ReflectionUtils.makeAccessible(this.field); return this.field.get(getWrappedInstance()); diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java index 5cd244f80f9c..d8868153520f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfo.java @@ -36,8 +36,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -185,8 +185,7 @@ else if (existingPd instanceof IndexedPropertyDescriptor indexedPd) { } } - @Nullable - private PropertyDescriptor findExistingPropertyDescriptor(String propertyName, Class propertyType) { + private @Nullable PropertyDescriptor findExistingPropertyDescriptor(String propertyName, Class propertyType) { for (PropertyDescriptor pd : this.propertyDescriptors) { final Class candidateType; final String candidateName = pd.getName(); @@ -265,17 +264,13 @@ public MethodDescriptor[] getMethodDescriptors() { */ static class SimplePropertyDescriptor extends PropertyDescriptor { - @Nullable - private Method readMethod; + private @Nullable Method readMethod; - @Nullable - private Method writeMethod; + private @Nullable Method writeMethod; - @Nullable - private Class propertyType; + private @Nullable Class propertyType; - @Nullable - private Class propertyEditorClass; + private @Nullable Class propertyEditorClass; public SimplePropertyDescriptor(PropertyDescriptor original) throws IntrospectionException { this(original.getName(), original.getReadMethod(), original.getWriteMethod()); @@ -292,8 +287,7 @@ public SimplePropertyDescriptor(String propertyName, @Nullable Method readMethod } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @@ -303,8 +297,7 @@ public void setReadMethod(@Nullable Method readMethod) { } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -314,8 +307,7 @@ public void setWriteMethod(@Nullable Method writeMethod) { } @Override - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { if (this.propertyType == null) { try { this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod); @@ -328,8 +320,7 @@ public Class getPropertyType() { } @Override - @Nullable - public Class getPropertyEditorClass() { + public @Nullable Class getPropertyEditorClass() { return this.propertyEditorClass; } @@ -362,26 +353,19 @@ public String toString() { */ static class SimpleIndexedPropertyDescriptor extends IndexedPropertyDescriptor { - @Nullable - private Method readMethod; + private @Nullable Method readMethod; - @Nullable - private Method writeMethod; + private @Nullable Method writeMethod; - @Nullable - private Class propertyType; + private @Nullable Class propertyType; - @Nullable - private Method indexedReadMethod; + private @Nullable Method indexedReadMethod; - @Nullable - private Method indexedWriteMethod; + private @Nullable Method indexedWriteMethod; - @Nullable - private Class indexedPropertyType; + private @Nullable Class indexedPropertyType; - @Nullable - private Class propertyEditorClass; + private @Nullable Class propertyEditorClass; public SimpleIndexedPropertyDescriptor(IndexedPropertyDescriptor original) throws IntrospectionException { this(original.getName(), original.getReadMethod(), original.getWriteMethod(), @@ -404,8 +388,7 @@ public SimpleIndexedPropertyDescriptor(String propertyName, @Nullable Method rea } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @@ -415,8 +398,7 @@ public void setReadMethod(@Nullable Method readMethod) { } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -426,8 +408,7 @@ public void setWriteMethod(@Nullable Method writeMethod) { } @Override - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { if (this.propertyType == null) { try { this.propertyType = PropertyDescriptorUtils.findPropertyType(this.readMethod, this.writeMethod); @@ -440,8 +421,7 @@ public Class getPropertyType() { } @Override - @Nullable - public Method getIndexedReadMethod() { + public @Nullable Method getIndexedReadMethod() { return this.indexedReadMethod; } @@ -451,8 +431,7 @@ public void setIndexedReadMethod(@Nullable Method indexedReadMethod) throws Intr } @Override - @Nullable - public Method getIndexedWriteMethod() { + public @Nullable Method getIndexedWriteMethod() { return this.indexedWriteMethod; } @@ -462,8 +441,7 @@ public void setIndexedWriteMethod(@Nullable Method indexedWriteMethod) throws In } @Override - @Nullable - public Class getIndexedPropertyType() { + public @Nullable Class getIndexedPropertyType() { if (this.indexedPropertyType == null) { try { this.indexedPropertyType = PropertyDescriptorUtils.findIndexedPropertyType( @@ -477,8 +455,7 @@ public Class getIndexedPropertyType() { } @Override - @Nullable - public Class getPropertyEditorClass() { + public @Nullable Class getPropertyEditorClass() { return this.propertyEditorClass; } diff --git a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java index 5f41742632d9..49b22dcbd907 100644 --- a/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/ExtendedBeanInfoFactory.java @@ -20,8 +20,9 @@ import java.beans.IntrospectionException; import java.lang.reflect.Method; +import org.jspecify.annotations.NonNull; + import org.springframework.core.Ordered; -import org.springframework.lang.NonNull; /** * Extension of {@link StandardBeanInfoFactory} that supports "non-standard" @@ -43,8 +44,7 @@ public class ExtendedBeanInfoFactory extends StandardBeanInfoFactory { @Override - @NonNull - public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { + public @NonNull BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { BeanInfo beanInfo = super.getBeanInfo(beanClass); return (supports(beanClass) ? new ExtendedBeanInfo(beanInfo) : beanInfo); } diff --git a/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java b/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java index 78d9e5c62f39..f5ffb2447606 100644 --- a/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java +++ b/spring-beans/src/main/java/org/springframework/beans/FatalBeanException.java @@ -16,7 +16,7 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown on an unrecoverable problem encountered in the diff --git a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java index a8247f6e421c..b1f4d72c7b06 100644 --- a/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java +++ b/spring-beans/src/main/java/org/springframework/beans/GenericTypeAwarePropertyDescriptor.java @@ -24,13 +24,13 @@ import java.util.Set; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.BridgeMethodResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.Property; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -47,34 +47,25 @@ final class GenericTypeAwarePropertyDescriptor extends PropertyDescriptor { private final Class beanClass; - @Nullable - private final Method readMethod; + private final @Nullable Method readMethod; - @Nullable - private final Method writeMethod; + private final @Nullable Method writeMethod; - @Nullable - private Set ambiguousWriteMethods; + private @Nullable Set ambiguousWriteMethods; private volatile boolean ambiguousWriteMethodsLogged; - @Nullable - private MethodParameter writeMethodParameter; + private @Nullable MethodParameter writeMethodParameter; - @Nullable - private volatile ResolvableType writeMethodType; + private volatile @Nullable ResolvableType writeMethodType; - @Nullable - private ResolvableType readMethodType; + private @Nullable ResolvableType readMethodType; - @Nullable - private volatile TypeDescriptor typeDescriptor; + private volatile @Nullable TypeDescriptor typeDescriptor; - @Nullable - private Class propertyType; + private @Nullable Class propertyType; - @Nullable - private final Class propertyEditorClass; + private final @Nullable Class propertyEditorClass; public GenericTypeAwarePropertyDescriptor(Class beanClass, String propertyName, @@ -136,14 +127,12 @@ public Class getBeanClass() { } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -158,8 +147,7 @@ public Method getWriteMethodForActualAccess() { return this.writeMethod; } - @Nullable - public Method getWriteMethodFallback(@Nullable Class valueType) { + public @Nullable Method getWriteMethodFallback(@Nullable Class valueType) { if (this.ambiguousWriteMethods != null) { for (Method method : this.ambiguousWriteMethods) { Class paramType = method.getParameterTypes()[0]; @@ -171,8 +159,7 @@ public Method getWriteMethodFallback(@Nullable Class valueType) { return null; } - @Nullable - public Method getUniqueWriteMethodFallback() { + public @Nullable Method getUniqueWriteMethodFallback() { if (this.ambiguousWriteMethods != null && this.ambiguousWriteMethods.size() == 1) { return this.ambiguousWriteMethods.iterator().next(); } @@ -213,14 +200,12 @@ public TypeDescriptor getTypeDescriptor() { } @Override - @Nullable - public Class getPropertyType() { + public @Nullable Class getPropertyType() { return this.propertyType; } @Override - @Nullable - public Class getPropertyEditorClass() { + public @Nullable Class getPropertyEditorClass() { return this.propertyEditorClass; } diff --git a/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java b/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java index c0d0f50adbbb..8484bdb613ae 100644 --- a/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/InvalidPropertyException.java @@ -16,7 +16,7 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception thrown when referring to an invalid bean property. diff --git a/spring-beans/src/main/java/org/springframework/beans/Mergeable.java b/spring-beans/src/main/java/org/springframework/beans/Mergeable.java index 41d50521f443..e55ba9c598b4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/Mergeable.java +++ b/spring-beans/src/main/java/org/springframework/beans/Mergeable.java @@ -16,7 +16,7 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface representing an object whose value set can be merged with diff --git a/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java b/spring-beans/src/main/java/org/springframework/beans/MethodInvocationException.java index 327643cbbf3f..d84e123b8017 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. @@ -18,13 +18,14 @@ import java.beans.PropertyChangeEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown when a bean property getter or setter method throws an exception, * 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/MutablePropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java index f52780e0ec9d..b2fe8dfb9bdf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/MutablePropertyValues.java @@ -27,7 +27,8 @@ import java.util.Spliterator; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -45,8 +46,7 @@ public class MutablePropertyValues implements PropertyValues, Serializable { private final List propertyValueList; - @Nullable - private Set processedProperties; + private @Nullable Set processedProperties; private volatile boolean converted; @@ -268,8 +268,7 @@ public PropertyValue[] getPropertyValues() { } @Override - @Nullable - public PropertyValue getPropertyValue(String propertyName) { + public @Nullable PropertyValue getPropertyValue(String propertyName) { for (PropertyValue pv : this.propertyValueList) { if (pv.getName().equals(propertyName)) { return pv; @@ -286,8 +285,7 @@ public PropertyValue getPropertyValue(String propertyName) { * @see #getPropertyValue(String) * @see PropertyValue#getValue() */ - @Nullable - public Object get(String propertyName) { + public @Nullable Object get(String propertyName) { PropertyValue pv = getPropertyValue(propertyName); return (pv != null ? pv.getValue() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java b/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java index 79e017e89ac7..8f7c63c95019 100644 --- a/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/NotWritablePropertyException.java @@ -16,7 +16,7 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception thrown on an attempt to set the value of a property that @@ -29,8 +29,7 @@ @SuppressWarnings("serial") public class NotWritablePropertyException extends InvalidPropertyException { - @Nullable - private final String[] possibleMatches; + private final String @Nullable [] possibleMatches; /** @@ -86,8 +85,7 @@ public NotWritablePropertyException(Class beanClass, String propertyName, Str * Return suggestions for actual bean property names that closely match * the invalid property name, if any. */ - @Nullable - public String[] getPossibleMatches() { + public String @Nullable [] getPossibleMatches() { return this.possibleMatches; } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java index 7789437b551a..8e148adc4041 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessException.java @@ -18,7 +18,7 @@ import java.beans.PropertyChangeEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Superclass for exceptions related to a property access, @@ -30,8 +30,7 @@ @SuppressWarnings("serial") public abstract class PropertyAccessException extends BeansException { - @Nullable - private final PropertyChangeEvent propertyChangeEvent; + private final @Nullable PropertyChangeEvent propertyChangeEvent; /** @@ -61,24 +60,21 @@ public PropertyAccessException(String msg, @Nullable Throwable cause) { *

May be {@code null}; only available if an actual bean property * was affected. */ - @Nullable - public PropertyChangeEvent getPropertyChangeEvent() { + public @Nullable PropertyChangeEvent getPropertyChangeEvent() { return this.propertyChangeEvent; } /** * Return the name of the affected property, if available. */ - @Nullable - public String getPropertyName() { + public @Nullable String getPropertyName() { return (this.propertyChangeEvent != null ? this.propertyChangeEvent.getPropertyName() : null); } /** * Return the affected value that was about to be set, if any. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return (this.propertyChangeEvent != null ? this.propertyChangeEvent.getNewValue() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java index 03201a89d0d7..885dee2ffa2c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessor.java @@ -18,8 +18,9 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * Common interface for classes that can access named properties @@ -101,8 +102,7 @@ public interface PropertyAccessor { * @throws PropertyAccessException if the property was valid but the * accessor method failed */ - @Nullable - Class getPropertyType(String propertyName) throws BeansException; + @Nullable Class getPropertyType(String propertyName) throws BeansException; /** * Return a type descriptor for the specified property: @@ -114,8 +114,7 @@ public interface PropertyAccessor { * @throws PropertyAccessException if the property was valid but the * accessor method failed */ - @Nullable - TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException; + @Nullable TypeDescriptor getPropertyTypeDescriptor(String propertyName) throws BeansException; /** * Get the current value of the specified property. @@ -127,8 +126,7 @@ public interface PropertyAccessor { * @throws PropertyAccessException if the property was valid but the * accessor method failed */ - @Nullable - Object getPropertyValue(String propertyName) throws BeansException; + @Nullable Object getPropertyValue(String propertyName) throws BeansException; /** * Set the specified value as current property value. diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java index 465b3ff8b1d8..73b39e1ca8d1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyAccessorUtils.java @@ -16,7 +16,7 @@ package org.springframework.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility methods for classes that perform bean property access @@ -173,8 +173,7 @@ public static String canonicalPropertyName(@Nullable String propertyName) { * (as array of the same size) * @see #canonicalPropertyName(String) */ - @Nullable - public static String[] canonicalPropertyNames(@Nullable String[] propertyNames) { + public static String @Nullable [] canonicalPropertyNames(String @Nullable [] propertyNames) { if (propertyNames == null) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java b/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java index 46491e0d2b88..f4cb0db73d2b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyBatchUpdateException.java @@ -20,7 +20,8 @@ import java.io.PrintWriter; import java.util.StringJoiner; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -73,8 +74,7 @@ public final PropertyAccessException[] getPropertyAccessExceptions() { /** * Return the exception for this field, or {@code null} if there isn't any. */ - @Nullable - public PropertyAccessException getPropertyAccessException(String propertyName) { + public @Nullable PropertyAccessException getPropertyAccessException(String propertyName) { for (PropertyAccessException pae : this.propertyAccessExceptions) { if (ObjectUtils.nullSafeEquals(propertyName, pae.getPropertyName())) { return pae; diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java index 90c2aef90275..0feea7208617 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyDescriptorUtils.java @@ -26,7 +26,8 @@ import java.util.Map; import java.util.TreeMap; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -141,8 +142,7 @@ public static void copyNonMethodProperties(PropertyDescriptor source, PropertyDe /** * See {@link java.beans.PropertyDescriptor#findPropertyType}. */ - @Nullable - public static Class findPropertyType(@Nullable Method readMethod, @Nullable Method writeMethod) + public static @Nullable Class findPropertyType(@Nullable Method readMethod, @Nullable Method writeMethod) throws IntrospectionException { Class propertyType = null; @@ -186,8 +186,7 @@ else if (params[0].isAssignableFrom(propertyType)) { /** * See {@link java.beans.IndexedPropertyDescriptor#findIndexedPropertyType}. */ - @Nullable - public static Class findIndexedPropertyType(String name, @Nullable Class propertyType, + public static @Nullable Class findIndexedPropertyType(String name, @Nullable Class propertyType, @Nullable Method indexedReadMethod, @Nullable Method indexedWriteMethod) throws IntrospectionException { Class indexedPropertyType = null; @@ -264,11 +263,9 @@ public static boolean equals(PropertyDescriptor pd, PropertyDescriptor otherPd) */ private static class BasicPropertyDescriptor extends PropertyDescriptor { - @Nullable - private Method readMethod; + private @Nullable Method readMethod; - @Nullable - private Method writeMethod; + private @Nullable Method writeMethod; private final List alternativeWriteMethods = new ArrayList<>(); @@ -284,8 +281,7 @@ public void setReadMethod(@Nullable Method readMethod) { } @Override - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } @@ -303,8 +299,7 @@ public void addWriteMethod(Method writeMethod) { } @Override - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { if (this.writeMethod == null && !this.alternativeWriteMethods.isEmpty()) { if (this.readMethod == null) { return this.alternativeWriteMethods.get(0); diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java index 9cbbc55ca575..8d66df82cc96 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistry.java @@ -18,7 +18,7 @@ import java.beans.PropertyEditor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Encapsulates methods for registering JavaBeans {@link PropertyEditor PropertyEditors}. @@ -76,7 +76,6 @@ public interface PropertyEditorRegistry { * {@code null} if looking for an editor for all properties of the given type * @return the registered editor, or {@code null} if none */ - @Nullable - PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath); + @Nullable PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath); } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java index 0802b5788c75..dc07ebd707ff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyEditorRegistrySupport.java @@ -44,6 +44,7 @@ import java.util.UUID; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.xml.sax.InputSource; import org.springframework.beans.propertyeditors.ByteArrayPropertyEditor; @@ -74,7 +75,6 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourceArrayPropertyEditor; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -92,30 +92,24 @@ */ public class PropertyEditorRegistrySupport implements PropertyEditorRegistry { - @Nullable - private ConversionService conversionService; + private @Nullable ConversionService conversionService; private boolean defaultEditorsActive = false; private boolean configValueEditorsActive = false; - @Nullable - private PropertyEditorRegistrar defaultEditorRegistrar; + private @Nullable PropertyEditorRegistrar defaultEditorRegistrar; - @Nullable + @SuppressWarnings("NullAway.Init") private Map, PropertyEditor> defaultEditors; - @Nullable - private Map, PropertyEditor> overriddenDefaultEditors; + private @Nullable Map, PropertyEditor> overriddenDefaultEditors; - @Nullable - private Map, PropertyEditor> customEditors; + private @Nullable Map, PropertyEditor> customEditors; - @Nullable - private Map customEditorsForPath; + private @Nullable Map customEditorsForPath; - @Nullable - private Map, PropertyEditor> customEditorCache; + private @Nullable Map, PropertyEditor> customEditorCache; /** @@ -129,8 +123,7 @@ public void setConversionService(@Nullable ConversionService conversionService) /** * Return the associated ConversionService, if any. */ - @Nullable - public ConversionService getConversionService() { + public @Nullable ConversionService getConversionService() { return this.conversionService; } @@ -194,9 +187,7 @@ public void overrideDefaultEditor(Class requiredType, PropertyEditor property * @return the default editor, or {@code null} if none found * @see #registerDefaultEditors */ - @Nullable - @SuppressWarnings("NullAway") - public PropertyEditor getDefaultEditor(Class requiredType) { + public @Nullable PropertyEditor getDefaultEditor(Class requiredType) { if (!this.defaultEditorsActive) { return null; } @@ -331,8 +322,7 @@ public void registerCustomEditor(@Nullable Class requiredType, @Nullable Stri } @Override - @Nullable - public PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath) { + public @Nullable PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath) { Class requiredTypeToUse = requiredType; if (propertyPath != null) { if (this.customEditorsForPath != null) { @@ -391,8 +381,7 @@ public boolean hasCustomEditorForElement(@Nullable Class elementType, @Nullab * @return the type of the property, or {@code null} if not determinable * @see BeanWrapper#getPropertyType(String) */ - @Nullable - protected Class getPropertyType(String propertyPath) { + protected @Nullable Class getPropertyType(String propertyPath) { return null; } @@ -402,8 +391,7 @@ protected Class getPropertyType(String propertyPath) { * @param requiredType the type to look for * @return the custom editor, or {@code null} if none specific for this property */ - @Nullable - private PropertyEditor getCustomEditor(String propertyName, @Nullable Class requiredType) { + private @Nullable PropertyEditor getCustomEditor(String propertyName, @Nullable Class requiredType) { CustomEditorHolder holder = (this.customEditorsForPath != null ? this.customEditorsForPath.get(propertyName) : null); return (holder != null ? holder.getPropertyEditor(requiredType) : null); @@ -417,8 +405,7 @@ private PropertyEditor getCustomEditor(String propertyName, @Nullable Class r * @return the custom editor, or {@code null} if none found for this type * @see java.beans.PropertyEditor#getAsText() */ - @Nullable - private PropertyEditor getCustomEditor(@Nullable Class requiredType) { + private @Nullable PropertyEditor getCustomEditor(@Nullable Class requiredType) { if (requiredType == null || this.customEditors == null) { return null; } @@ -457,8 +444,7 @@ private PropertyEditor getCustomEditor(@Nullable Class requiredType) { * @param propertyName the name of the property * @return the property type, or {@code null} if not determinable */ - @Nullable - protected Class guessPropertyTypeFromEditors(String propertyName) { + protected @Nullable Class guessPropertyTypeFromEditors(String propertyName) { if (this.customEditorsForPath != null) { CustomEditorHolder editorHolder = this.customEditorsForPath.get(propertyName); if (editorHolder == null) { @@ -545,8 +531,7 @@ private static final class CustomEditorHolder { private final PropertyEditor propertyEditor; - @Nullable - private final Class registeredType; + private final @Nullable Class registeredType; private CustomEditorHolder(PropertyEditor propertyEditor, @Nullable Class registeredType) { this.propertyEditor = propertyEditor; @@ -557,13 +542,11 @@ private PropertyEditor getPropertyEditor() { return this.propertyEditor; } - @Nullable - private Class getRegisteredType() { + private @Nullable Class getRegisteredType() { return this.registeredType; } - @Nullable - private PropertyEditor getPropertyEditor(@Nullable Class requiredType) { + private @Nullable PropertyEditor getPropertyEditor(@Nullable Class requiredType) { // Special case: If no required type specified, which usually only happens for // Collection elements, or required type is not assignable to registered type, // which usually only happens for generic properties of type Object - diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java index 00f567b0f67b..1a867fc5b513 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValue.java @@ -18,7 +18,8 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -44,23 +45,19 @@ public class PropertyValue extends BeanMetadataAttributeAccessor implements Seri private final String name; - @Nullable - private final Object value; + private final @Nullable Object value; private boolean optional = false; private boolean converted = false; - @Nullable - private Object convertedValue; + private @Nullable Object convertedValue; /** Package-visible field that indicates whether conversion is necessary. */ - @Nullable - volatile Boolean conversionNecessary; + volatile @Nullable Boolean conversionNecessary; /** Package-visible field for caching the resolved property path tokens. */ - @Nullable - transient volatile Object resolvedTokens; + transient volatile @Nullable Object resolvedTokens; /** @@ -122,8 +119,7 @@ public String getName() { * It is the responsibility of the BeanWrapper implementation to * perform type conversion. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } @@ -181,8 +177,7 @@ public synchronized void setConvertedValue(@Nullable Object value) { * Return the converted value of this property value, * after processed type conversion. */ - @Nullable - public synchronized Object getConvertedValue() { + public synchronized @Nullable Object getConvertedValue() { return this.convertedValue; } diff --git a/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java b/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java index b754a32a0f60..4134bc707281 100644 --- a/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/PropertyValues.java @@ -23,7 +23,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Holder containing one or more {@link PropertyValue} objects, @@ -72,8 +72,7 @@ default Stream stream() { * @param propertyName the name to search for * @return the property value, or {@code null} if none */ - @Nullable - PropertyValue getPropertyValue(String propertyName); + @Nullable PropertyValue getPropertyValue(String propertyName); /** * Return the changes since the previous PropertyValues. diff --git a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java index 75c9a699bb69..45f1b09cd895 100644 --- a/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/SimpleBeanInfoFactory.java @@ -24,7 +24,6 @@ import java.util.Collection; import org.springframework.core.Ordered; -import org.springframework.lang.NonNull; /** * {@link BeanInfoFactory} implementation that bypasses the standard {@link java.beans.Introspector} @@ -47,7 +46,6 @@ class SimpleBeanInfoFactory implements BeanInfoFactory, Ordered { @Override - @NonNull public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { Collection pds = PropertyDescriptorUtils.determineBasicProperties(beanClass); diff --git a/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java index d93d8d6a6905..feda722741f2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/StandardBeanInfoFactory.java @@ -22,7 +22,6 @@ import org.springframework.core.Ordered; import org.springframework.core.SpringProperties; -import org.springframework.lang.NonNull; /** * {@link BeanInfoFactory} implementation that performs standard @@ -66,7 +65,6 @@ public class StandardBeanInfoFactory implements BeanInfoFactory, Ordered { @Override - @NonNull public BeanInfo getBeanInfo(Class beanClass) throws IntrospectionException { BeanInfo beanInfo = (shouldIntrospectorIgnoreBeaninfoClasses ? Introspector.getBeanInfo(beanClass, Introspector.IGNORE_ALL_BEANINFO) : diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java index 200a350727aa..a4ac581a2daa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverter.java @@ -18,9 +18,10 @@ import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * Interface that defines type conversion methods. Typically (but not necessarily) @@ -51,8 +52,7 @@ public interface TypeConverter { * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException; + @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException; /** * Convert the value to the required type (if necessary from a String). @@ -70,8 +70,7 @@ public interface TypeConverter { * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException; /** @@ -90,8 +89,7 @@ T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException; /** @@ -110,8 +108,7 @@ T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType * @see org.springframework.core.convert.ConversionService * @see org.springframework.core.convert.converter.Converter */ - @Nullable - default T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + default @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { throw new UnsupportedOperationException("TypeDescriptor resolution not supported"); diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java index f41724275445..54aaa607f075 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterDelegate.java @@ -28,12 +28,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.NumberUtils; @@ -60,8 +60,7 @@ class TypeConverterDelegate { private final PropertyEditorRegistrySupport propertyEditorRegistry; - @Nullable - private final Object targetObject; + private final @Nullable Object targetObject; /** @@ -93,8 +92,7 @@ public TypeConverterDelegate(PropertyEditorRegistrySupport propertyEditorRegistr * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */ - @Nullable - public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, + public @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, Object newValue, @Nullable Class requiredType) throws IllegalArgumentException { return convertIfNecessary(propertyName, oldValue, newValue, requiredType, TypeDescriptor.valueOf(requiredType)); @@ -113,8 +111,7 @@ public T convertIfNecessary(@Nullable String propertyName, @Nullable Object * @throws IllegalArgumentException if type conversion failed */ @SuppressWarnings("unchecked") - @Nullable - public T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, + public @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws IllegalArgumentException { // Custom editor for this type? @@ -337,8 +334,7 @@ private Object attemptToConvertStringToEnum(Class requiredType, String trimme * @param requiredType the type to find an editor for * @return the corresponding editor, or {@code null} if none */ - @Nullable - private PropertyEditor findDefaultEditor(@Nullable Class requiredType) { + private @Nullable PropertyEditor findDefaultEditor(@Nullable Class requiredType) { PropertyEditor editor = null; if (requiredType != null) { // No custom editor -> check BeanWrapperImpl's default editors. @@ -362,8 +358,7 @@ private PropertyEditor findDefaultEditor(@Nullable Class requiredType) { * @return the new value, possibly the result of type conversion * @throws IllegalArgumentException if type conversion failed */ - @Nullable - private Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue, + private @Nullable Object doConvertValue(@Nullable Object oldValue, @Nullable Object newValue, @Nullable Class requiredType, @Nullable PropertyEditor editor) { Object convertedValue = newValue; @@ -628,15 +623,13 @@ private Collection convertToTypedCollection(Collection original, @Nullable return (originalAllowed ? original : convertedCopy); } - @Nullable - private String buildIndexedPropertyName(@Nullable String propertyName, int index) { + private @Nullable String buildIndexedPropertyName(@Nullable String propertyName, int index) { return (propertyName != null ? propertyName + PropertyAccessor.PROPERTY_KEY_PREFIX + index + PropertyAccessor.PROPERTY_KEY_SUFFIX : null); } - @Nullable - private String buildKeyedPropertyName(@Nullable String propertyName, Object key) { + private @Nullable String buildKeyedPropertyName(@Nullable String propertyName, Object key) { return (propertyName != null ? propertyName + PropertyAccessor.PROPERTY_KEY_PREFIX + key + PropertyAccessor.PROPERTY_KEY_SUFFIX : null); diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java index 2351382512c9..5bbe75ac3500 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeConverterSupport.java @@ -18,11 +18,12 @@ import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConverterNotFoundException; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -35,19 +36,16 @@ */ public abstract class TypeConverterSupport extends PropertyEditorRegistrySupport implements TypeConverter { - @Nullable - TypeConverterDelegate typeConverterDelegate; + @Nullable TypeConverterDelegate typeConverterDelegate; @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { return convertIfNecessary(null, value, requiredType, TypeDescriptor.valueOf(requiredType)); } @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { return convertIfNecessary((methodParam != null ? methodParam.getParameterName() : null), value, requiredType, @@ -55,8 +53,7 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi } @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException { return convertIfNecessary((field != null ? field.getName() : null), value, requiredType, @@ -64,15 +61,13 @@ public T convertIfNecessary(@Nullable Object value, @Nullable Class requi } @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { return convertIfNecessary(null, value, requiredType, typeDescriptor); } - @Nullable - private T convertIfNecessary(@Nullable String propertyName, @Nullable Object value, + private @Nullable T convertIfNecessary(@Nullable String propertyName, @Nullable Object value, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { Assert.state(this.typeConverterDelegate != null, "No TypeConverterDelegate"); diff --git a/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java index ccfa6a003050..88e31532bb23 100644 --- a/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java +++ b/spring-beans/src/main/java/org/springframework/beans/TypeMismatchException.java @@ -18,7 +18,8 @@ import java.beans.PropertyChangeEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -37,14 +38,11 @@ public class TypeMismatchException extends PropertyAccessException { public static final String ERROR_CODE = "typeMismatch"; - @Nullable - private String propertyName; + private @Nullable String propertyName; - @Nullable - private final transient Object value; + private final transient @Nullable Object value; - @Nullable - private final Class requiredType; + private final @Nullable Class requiredType; /** @@ -123,8 +121,7 @@ public void initPropertyName(String propertyName) { * Return the name of the affected property, if available. */ @Override - @Nullable - public String getPropertyName() { + public @Nullable String getPropertyName() { return this.propertyName; } @@ -132,16 +129,14 @@ public String getPropertyName() { * Return the offending value (may be {@code null}). */ @Override - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } /** * Return the required target type, if any. */ - @Nullable - public Class getRequiredType() { + public @Nullable Class getRequiredType() { return this.requiredType; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java index 9290a7153722..ba00f28020b3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanCreationException.java @@ -21,9 +21,10 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.FatalBeanException; import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * Exception thrown when a BeanFactory encounters an error when @@ -34,14 +35,11 @@ @SuppressWarnings("serial") public class BeanCreationException extends FatalBeanException { - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private final String resourceDescription; + private final @Nullable String resourceDescription; - @Nullable - private List relatedCauses; + private @Nullable List relatedCauses; /** @@ -120,16 +118,14 @@ public BeanCreationException(@Nullable String resourceDescription, String beanNa * Return the description of the resource that the bean * definition came from, if any. */ - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return this.resourceDescription; } /** * Return the name of the bean requested, if any. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } @@ -150,8 +146,7 @@ public void addRelatedCause(Throwable ex) { * Return the related causes, if any. * @return the array of related causes, or {@code null} if none */ - @Nullable - public Throwable[] getRelatedCauses() { + public Throwable @Nullable [] getRelatedCauses() { if (this.relatedCauses == null) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java index f97c34b086e2..9f7330dec9eb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanDefinitionStoreException.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.FatalBeanException; -import org.springframework.lang.Nullable; /** * Exception thrown when a BeanFactory encounters an invalid bean definition: @@ -30,11 +31,9 @@ @SuppressWarnings("serial") public class BeanDefinitionStoreException extends FatalBeanException { - @Nullable - private final String resourceDescription; + private final @Nullable String resourceDescription; - @Nullable - private final String beanName; + private final @Nullable String beanName; /** @@ -101,9 +100,11 @@ public BeanDefinitionStoreException(@Nullable String resourceDescription, String * @param cause the root cause (may be {@code null}) */ public BeanDefinitionStoreException( - @Nullable String resourceDescription, String beanName, String msg, @Nullable Throwable cause) { + @Nullable String resourceDescription, String beanName, @Nullable String msg, @Nullable Throwable cause) { - super("Invalid bean definition with name '" + beanName + "' defined in " + resourceDescription + ": " + msg, + super(msg == null ? + "Invalid bean definition with name '" + beanName + "' defined in " + resourceDescription : + "Invalid bean definition with name '" + beanName + "' defined in " + resourceDescription + ": " + msg, cause); this.resourceDescription = resourceDescription; this.beanName = beanName; @@ -113,16 +114,14 @@ public BeanDefinitionStoreException( /** * Return the description of the resource that the bean definition came from, if available. */ - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return this.resourceDescription; } /** * Return the name of the bean, if available. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } 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 fb1f3ffd9df2..27f795976275 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. @@ -16,9 +16,10 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * The root interface for accessing a Spring bean container. @@ -124,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. @@ -182,7 +190,7 @@ public interface BeanFactory { * @throws BeansException if the bean could not be created * @since 2.5 */ - Object getBean(String name, Object... args) throws BeansException; + Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException; /** * Return the bean instance that uniquely matches the given object type, if any. @@ -220,7 +228,7 @@ public interface BeanFactory { * @throws BeansException if the bean could not be created * @since 4.1 */ - T getBean(Class requiredType, Object... args) throws BeansException; + T getBean(Class requiredType, @Nullable Object @Nullable ... args) throws BeansException; /** * Return a provider for the specified bean, allowing for lazy on-demand retrieval @@ -357,8 +365,7 @@ public interface BeanFactory { * @see #getBean * @see #isTypeMatch */ - @Nullable - Class getType(String name) throws NoSuchBeanDefinitionException; + @Nullable Class getType(String name) throws NoSuchBeanDefinitionException; /** * Determine the type of the bean with the given name. More specifically, @@ -378,8 +385,7 @@ public interface BeanFactory { * @see #getBean * @see #isTypeMatch */ - @Nullable - Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException; + @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException; /** * Return the aliases for the given bean name, if any. 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 24227a3c1143..3f6921f22eee 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 @@ -24,9 +24,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -72,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); } /** @@ -84,14 +85,14 @@ public static boolean isFactoryDereference(@Nullable String name) { */ 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/BeanRegistrar.java b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java new file mode 100644 index 000000000000..753840226dbe --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistrar.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2025 the original author 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.beans.factory; + +import org.springframework.core.env.Environment; + +/** + * Contract for registering beans programmatically, typically imported with an + * {@link org.springframework.context.annotation.Import @Import} annotation on + * a {@link org.springframework.context.annotation.Configuration @Configuration} + * class. + *

+ * @Configuration
+ * @Import(MyBeanRegistrar.class)
+ * class MyConfiguration {
+ * }
+ * Can also be applied to an application context via + * {@link org.springframework.context.support.GenericApplicationContext#register(BeanRegistrar...)}. + * + * + *

Bean registrar implementations use {@link BeanRegistry} and {@link Environment} + * APIs to register beans programmatically in a concise and flexible way. + *

+ * class MyBeanRegistrar implements BeanRegistrar {
+ *
+ *     @Override
+ *     public void register(BeanRegistry registry, Environment env) {
+ *         registry.registerBean("foo", Foo.class);
+ *         registry.registerBean("bar", Bar.class, spec -> spec
+ *                 .prototype()
+ *                 .lazyInit()
+ *                 .description("Custom description")
+ *                 .supplier(context -> new Bar(context.bean(Foo.class))));
+ *         if (env.matchesProfiles("baz")) {
+ *             registry.registerBean(Baz.class, spec -> spec
+ *                     .supplier(context -> new Baz("Hello World!")));
+ *         }
+ *     }
+ * }
+ * + *

A {@code BeanRegistrar} implementing {@link org.springframework.context.annotation.ImportAware} + * can optionally introspect import metadata when used in an import scenario, otherwise the + * {@code setImportMetadata} method is simply not being called. + * + *

In Kotlin, it is recommended to use {@code BeanRegistrarDsl} instead of + * implementing {@code BeanRegistrar}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +@FunctionalInterface +public interface BeanRegistrar { + + /** + * Register beans on the given {@link BeanRegistry} in a programmatic way. + * @param registry the bean registry to operate on + * @param env the environment that can be used to get the active profile or some properties + */ + void register(BeanRegistry registry, Environment env); + +} 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 new file mode 100644 index 000000000000..b5eeec43cf66 --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/BeanRegistry.java @@ -0,0 +1,239 @@ +/* + * Copyright 2002-2025 the original author 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.beans.factory; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.Environment; + +/** + * Used in {@link BeanRegistrar#register(BeanRegistry, Environment)} to expose + * programmatic bean registration capabilities. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public interface BeanRegistry { + + /** + * Register beans using the given {@link BeanRegistrar}. + * @param registrar the bean registrar that will be called to register + * additional beans + */ + void register(BeanRegistrar registrar); + + /** + * Given a name, register an alias for it. + * @param name the canonical name + * @param alias the alias to be registered + * @throws IllegalStateException if the alias is already in use + * and may not be overridden + */ + 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. + * @param beanClass the class of the bean + * @return the generated bean name + */ + String registerBean(Class beanClass); + + /** + * 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. + * @param beanClass the class of the bean + * @param customizer callback to customize other bean properties than the name + * @return the generated bean name + */ + 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. + * @param name the name of the bean + * @param beanClass the class of the bean + */ + void registerBean(String name, Class beanClass); + + /** + * 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. + * @param name the name of the bean + * @param beanClass the class of the bean + * @param customizer callback to customize other bean properties than the name + */ + void registerBean(String name, Class beanClass, Consumer> customizer); + + + /** + * Specification for customizing a bean. + * @param the bean type + */ + interface Spec { + + /** + * Allow for instantiating this bean on a background thread. + * @see AbstractBeanDefinition#setBackgroundInit(boolean) + */ + Spec backgroundInit(); + + /** + * Set a human-readable description of this bean. + * @see BeanDefinition#setDescription(String) + */ + Spec description(String description); + + /** + * Configure this bean as a fallback autowire candidate. + * @see BeanDefinition#setFallback(boolean) + * @see #primary + */ + Spec fallback(); + + /** + * 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 + */ + Spec infrastructure(); + + /** + * Configure this bean as lazily initialized. + * @see BeanDefinition#setLazyInit(boolean) + */ + Spec lazyInit(); + + /** + * Configure this bean as not a candidate for getting autowired into another bean. + * @see BeanDefinition#setAutowireCandidate(boolean) + */ + Spec notAutowirable(); + + /** + * The sort order of this bean. This is analogous to the + * {@code @Order} annotation. + * @see AbstractBeanDefinition#ORDER_ATTRIBUTE + */ + Spec order(int order); + + /** + * Configure this bean as a primary autowire candidate. + * @see BeanDefinition#setPrimary(boolean) + * @see #fallback + */ + Spec primary(); + + /** + * Configure this bean with a prototype scope. + * @see BeanDefinition#setScope(String) + * @see BeanDefinition#SCOPE_PROTOTYPE + */ + Spec prototype(); + + /** + * Set the supplier to construct a bean instance. + * @see AbstractBeanDefinition#setInstanceSupplier(Supplier) + */ + Spec supplier(Function supplier); + + /** + * Set a generics-containing target type of this bean. + * @see #targetType(ResolvableType) + * @see RootBeanDefinition#setTargetType(ResolvableType) + */ + Spec targetType(ParameterizedTypeReference type); + + /** + * Set a generics-containing target type of this bean. + * @see #targetType(ParameterizedTypeReference) + * @see RootBeanDefinition#setTargetType(ResolvableType) + */ + Spec targetType(ResolvableType type); + } + + + /** + * Context available from the bean instance supplier designed to give access + * to bean dependencies. + */ + 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 an instance of the single bean matching the required type + * @see BeanFactory#getBean(String) + */ + T bean(Class requiredType) throws BeansException; + + /** + * Return an instance, which may be shared or independent, of the + * specified bean. + * @param name the name of the bean to retrieve + * @param requiredType type the bean must match; can be an interface or superclass + * @return an instance of the bean. + * @see BeanFactory#getBean(String, Class) + */ + T bean(String name, Class requiredType) throws BeansException; + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. + *

For matching a generic type, consider {@link #beanProvider(ResolvableType)}. + * @param requiredType type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + * @see BeanFactory#getBeanProvider(Class) + */ + ObjectProvider beanProvider(Class requiredType); + + /** + * Return a provider for the specified bean, allowing for lazy on-demand retrieval + * of instances, including availability and uniqueness options. This variant allows + * for specifying a generic type to match, similar to reflective injection points + * with generic type declarations in method/constructor parameters. + *

Note that collections of beans are not supported here, in contrast to reflective + * injection points. For programmatically retrieving a list of beans matching a + * specific type, specify the actual bean type as an argument here and subsequently + * use {@link ObjectProvider#orderedStream()} or its lazy streaming/iteration options. + *

Also, generics matching is strict here, as per the Java assignment rules. + * For lenient fallback matching with unchecked semantics (similar to the 'unchecked' + * Java compiler warning), consider calling {@link #beanProvider(Class)} with the + * raw type as a second step if no full generic match is + * {@link ObjectProvider#getIfAvailable() available} with this variant. + * @param requiredType type the bean must match; can be a generic type declaration + * @return a corresponding provider handle + * @see BeanFactory#getBeanProvider(ResolvableType) + */ + ObjectProvider beanProvider(ResolvableType requiredType); + } + +} diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java b/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java index fc26cc0ad55e..5cc7f2420ee0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/CannotLoadBeanClassException.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.FatalBeanException; -import org.springframework.lang.Nullable; /** * Exception thrown when the BeanFactory cannot load the specified class @@ -29,13 +30,11 @@ @SuppressWarnings("serial") public class CannotLoadBeanClassException extends FatalBeanException { - @Nullable - private final String resourceDescription; + private final @Nullable String resourceDescription; private final String beanName; - @Nullable - private final String beanClassName; + private final @Nullable String beanClassName; /** @@ -80,8 +79,7 @@ public CannotLoadBeanClassException(@Nullable String resourceDescription, String * Return the description of the resource that the bean * definition came from. */ - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return this.resourceDescription; } @@ -95,8 +93,7 @@ public String getBeanName() { /** * Return the name of the class we were trying to load. */ - @Nullable - public String getBeanClassName() { + public @Nullable String getBeanClassName() { return this.beanClassName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java index c8d06fe3b01c..17138b0d706b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/FactoryBean.java @@ -16,7 +16,7 @@ package org.springframework.beans.factory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to be implemented by objects used within a {@link BeanFactory} which @@ -92,8 +92,7 @@ public interface FactoryBean { * @throws Exception in case of creation errors * @see FactoryBeanNotInitializedException */ - @Nullable - T getObject() throws Exception; + @Nullable T getObject() throws Exception; /** * Return the type of object that this FactoryBean creates, @@ -114,8 +113,7 @@ public interface FactoryBean { * or {@code null} if not known at the time of the call * @see ListableBeanFactory#getBeansOfType */ - @Nullable - Class getObjectType(); + @Nullable Class getObjectType(); /** * Is the object managed by this factory a singleton? That is, diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java index d7504438bed8..3ed796bd1af3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/HierarchicalBeanFactory.java @@ -16,7 +16,7 @@ package org.springframework.beans.factory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Sub-interface implemented by bean factories that can be part @@ -36,8 +36,7 @@ public interface HierarchicalBeanFactory extends BeanFactory { /** * Return the parent bean factory, or {@code null} if there is none. */ - @Nullable - BeanFactory getParentBeanFactory(); + @Nullable BeanFactory getParentBeanFactory(); /** * Return whether the local bean factory contains a bean of the given name, diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java b/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java index f83a5da492f7..981a7109114a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/InjectionPoint.java @@ -22,8 +22,9 @@ import java.lang.reflect.Member; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -42,14 +43,11 @@ */ public class InjectionPoint { - @Nullable - protected MethodParameter methodParameter; + protected @Nullable MethodParameter methodParameter; - @Nullable - protected Field field; + protected @Nullable Field field; - @Nullable - private volatile Annotation[] fieldAnnotations; + private volatile Annotation @Nullable [] fieldAnnotations; /** @@ -93,8 +91,7 @@ protected InjectionPoint() { *

Note: Either MethodParameter or Field is available. * @return the MethodParameter, or {@code null} if none */ - @Nullable - public MethodParameter getMethodParameter() { + public @Nullable MethodParameter getMethodParameter() { return this.methodParameter; } @@ -103,8 +100,7 @@ public MethodParameter getMethodParameter() { *

Note: Either MethodParameter or Field is available. * @return the Field, or {@code null} if none */ - @Nullable - public Field getField() { + public @Nullable Field getField() { return this.field; } @@ -142,8 +138,7 @@ public Annotation[] getAnnotations() { * @return the annotation instance, or {@code null} if none found * @since 4.3.9 */ - @Nullable - public A getAnnotation(Class annotationType) { + public @Nullable A getAnnotation(Class annotationType) { return (this.field != null ? this.field.getAnnotation(annotationType) : obtainMethodParameter().getParameterAnnotation(annotationType)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java index 4ce86eceef84..097161fbaaf7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ListableBeanFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,9 +20,10 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * Extension of the {@link BeanFactory} interface to be implemented by bean factories @@ -145,8 +146,6 @@ public interface ListableBeanFactory extends BeanFactory { *

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Consider {@link #getBeanNamesForType(Class)} with selective {@link #getBean} + * calls for specific bean names in preference to this Map-based retrieval method. + * Aside from lazy instantiation benefits, this also avoids any exception suppression. * @param type the class or interface to match, or {@code null} for all concrete beans * @param includeNonSingletons whether to include prototype or scoped beans too * or just singletons (also applies to FactoryBeans) @@ -375,8 +374,7 @@ Map getBeansOfType(@Nullable Class type, boolean includeNonSin * @see #getBeansWithAnnotation(Class) * @see #getType(String) */ - @Nullable - A findAnnotationOnBean(String beanName, Class annotationType) + @Nullable A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException; /** @@ -397,8 +395,7 @@ A findAnnotationOnBean(String beanName, Class annotati * @see #getBeansWithAnnotation(Class) * @see #getType(String, boolean) */ - @Nullable - A findAnnotationOnBean( + @Nullable A findAnnotationOnBean( String beanName, Class annotationType, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java index 595b40ae982a..99f8a287e620 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/NoSuchBeanDefinitionException.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * Exception thrown when a {@code BeanFactory} is asked for a bean instance for which it @@ -35,11 +36,9 @@ @SuppressWarnings("serial") public class NoSuchBeanDefinitionException extends BeansException { - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private final ResolvableType resolvableType; + private final @Nullable ResolvableType resolvableType; /** @@ -107,8 +106,7 @@ public NoSuchBeanDefinitionException(ResolvableType type, String message) { /** * Return the name of the missing bean, if it was a lookup by name that failed. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } @@ -116,8 +114,7 @@ public String getBeanName() { * Return the required type of the missing bean, if it was a lookup by type * that failed. */ - @Nullable - public Class getBeanType() { + public @Nullable Class getBeanType() { return (this.resolvableType != null ? this.resolvableType.resolve() : null); } @@ -126,8 +123,7 @@ public Class getBeanType() { * by type that failed. * @since 4.3.4 */ - @Nullable - public ResolvableType getResolvableType() { + public @Nullable ResolvableType getResolvableType() { return this.resolvableType; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java b/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java index c2a6070c9d3f..88e62c0ffcb1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/NoUniqueBeanDefinitionException.java @@ -20,8 +20,9 @@ import java.util.Arrays; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -38,8 +39,7 @@ public class NoUniqueBeanDefinitionException extends NoSuchBeanDefinitionExcepti private final int numberOfBeansFound; - @Nullable - private final Collection beanNamesFound; + private final @Nullable Collection beanNamesFound; /** @@ -126,8 +126,7 @@ public int getNumberOfBeansFound() { * @since 4.3 * @see #getBeanType() */ - @Nullable - public Collection getBeanNamesFound() { + public @Nullable Collection getBeanNamesFound() { return this.beanNamesFound; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java index d62c3ee6d3cb..5f4c3353affe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/ObjectProvider.java @@ -22,9 +22,10 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.OrderComparator; -import org.springframework.lang.Nullable; /** * A variant of {@link ObjectFactory} designed specifically for injection points, @@ -104,7 +105,7 @@ default T getObject() throws BeansException { * @throws BeansException in case of creation errors * @see #getObject() */ - default T getObject(Object... args) throws BeansException { + default T getObject(@Nullable Object... args) throws BeansException { throw new UnsupportedOperationException("Retrieval with arguments not supported -" + "for custom ObjectProvider classes, implement getObject(Object...) for your purposes"); } @@ -116,8 +117,7 @@ default T getObject(Object... args) throws BeansException { * @throws BeansException in case of creation errors * @see #getObject() */ - @Nullable - default T getIfAvailable() throws BeansException { + default @Nullable T getIfAvailable() throws BeansException { try { return getObject(); } @@ -169,8 +169,7 @@ default void ifAvailable(Consumer dependencyConsumer) throws BeansException { * @throws BeansException in case of creation errors * @see #getObject() */ - @Nullable - default T getIfUnique() throws BeansException { + default @Nullable T getIfUnique() throws BeansException { try { return getObject(); } @@ -274,7 +273,7 @@ default Stream orderedStream() { * @see #orderedStream(Predicate) */ default Stream stream(Predicate> customFilter) { - return stream().filter(obj -> customFilter.test(obj.getClass())); + return stream(customFilter, true); } /** @@ -288,6 +287,44 @@ default Stream stream(Predicate> customFilter) { * @see #stream(Predicate) */ default Stream orderedStream(Predicate> customFilter) { + return orderedStream(customFilter, true); + } + + /** + * Return a custom-filtered {@link Stream} over all matching object instances, + * without specific ordering guarantees (but typically in registration order). + * @param customFilter a custom type filter for selecting beans among the raw + * bean type matches (or {@link #UNFILTERED} for all raw type matches without + * any default filtering) + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @since 6.2.5 + * @see #stream(Predicate) + * @see #orderedStream(Predicate, boolean) + */ + default Stream stream(Predicate> customFilter, boolean includeNonSingletons) { + if (!includeNonSingletons) { + throw new UnsupportedOperationException("Only supports includeNonSingletons=true by default"); + } + return stream().filter(obj -> customFilter.test(obj.getClass())); + } + + /** + * Return a custom-filtered {@link Stream} over all matching object instances, + * pre-ordered according to the factory's common order comparator. + * @param customFilter a custom type filter for selecting beans among the raw + * bean type matches (or {@link #UNFILTERED} for all raw type matches without + * any default filtering) + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @since 6.2.5 + * @see #orderedStream() + * @see #stream(Predicate) + */ + default Stream orderedStream(Predicate> customFilter, boolean includeNonSingletons) { + if (!includeNonSingletons) { + throw new UnsupportedOperationException("Only supports includeNonSingletons=true by default"); + } return orderedStream().filter(obj -> customFilter.test(obj.getClass())); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java index d11a5a16f823..c59a48ce0fdb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/UnsatisfiedDependencyException.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -32,8 +33,7 @@ @SuppressWarnings("serial") public class UnsatisfiedDependencyException extends BeanCreationException { - @Nullable - private final InjectionPoint injectionPoint; + private final @Nullable InjectionPoint injectionPoint; /** @@ -103,8 +103,7 @@ public UnsatisfiedDependencyException( * Return the injection point (field or method/constructor parameter), if known. * @since 4.3 */ - @Nullable - public InjectionPoint getInjectionPoint() { + public @Nullable InjectionPoint getInjectionPoint() { return this.injectionPoint; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java index 7d3fc7628a5b..b788b638eb46 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedBeanDefinition.java @@ -16,10 +16,11 @@ package org.springframework.beans.factory.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; -import org.springframework.lang.Nullable; /** * Extended {@link org.springframework.beans.factory.config.BeanDefinition} @@ -45,7 +46,6 @@ public interface AnnotatedBeanDefinition extends BeanDefinition { * @return the factory method metadata, or {@code null} if none * @since 4.1.1 */ - @Nullable - MethodMetadata getFactoryMethodMetadata(); + @Nullable MethodMetadata getFactoryMethodMetadata(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java index b8cb9070636c..4975b70612ea 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotatedGenericBeanDefinition.java @@ -16,11 +16,12 @@ package org.springframework.beans.factory.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.StandardAnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -45,8 +46,7 @@ public class AnnotatedGenericBeanDefinition extends GenericBeanDefinition implem private final AnnotationMetadata metadata; - @Nullable - private MethodMetadata factoryMethodMetadata; + private @Nullable MethodMetadata factoryMethodMetadata; /** @@ -100,8 +100,7 @@ public final AnnotationMetadata getMetadata() { } @Override - @Nullable - public final MethodMetadata getFactoryMethodMetadata() { + public final @Nullable MethodMetadata getFactoryMethodMetadata() { return this.factoryMethodMetadata; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java index b3550404c5c5..d344ca565f46 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AnnotationBeanWiringInfoResolver.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.wiring.BeanWiringInfo; import org.springframework.beans.factory.wiring.BeanWiringInfoResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -38,8 +39,7 @@ public class AnnotationBeanWiringInfoResolver implements BeanWiringInfoResolver { @Override - @Nullable - public BeanWiringInfo resolveWiringInfo(Object beanInstance) { + public @Nullable BeanWiringInfo resolveWiringInfo(Object beanInstance) { Assert.notNull(beanInstance, "Bean instance must not be null"); Configurable annotation = beanInstance.getClass().getAnnotation(Configurable.class); return (annotation != null ? buildWiringInfo(beanInstance, annotation) : null); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java index f711222c0f11..65559ead92c3 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanPostProcessor.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.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GeneratedClass; @@ -83,10 +84,8 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -103,8 +102,6 @@ * *

Also supports the common {@link jakarta.inject.Inject @Inject} annotation, * if available, as a direct alternative to Spring's own {@code @Autowired}. - * Additionally, it retains support for the {@code javax.inject.Inject} variant - * dating back to the original JSR-330 specification (as known from Java EE 6-8). * *

Autowired Constructors

*

Only one constructor of any given bean class may declare this annotation with @@ -173,11 +170,9 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA private int order = Ordered.LOWEST_PRECEDENCE - 2; - @Nullable - private ConfigurableListableBeanFactory beanFactory; + private @Nullable ConfigurableListableBeanFactory beanFactory; - @Nullable - private MetadataReaderFactory metadataReaderFactory; + private @Nullable MetadataReaderFactory metadataReaderFactory; private final Set lookupMethodsChecked = ConcurrentHashMap.newKeySet(256); @@ -189,8 +184,8 @@ public class AutowiredAnnotationBeanPostProcessor implements SmartInstantiationA /** * Create a new {@code AutowiredAnnotationBeanPostProcessor} for Spring's * standard {@link Autowired @Autowired} and {@link Value @Value} annotations. - *

Also supports the common {@link jakarta.inject.Inject @Inject} annotation, - * if available, as well as the original {@code javax.inject.Inject} variant. + *

Also supports the common {@link jakarta.inject.Inject @Inject} annotation + * if available. */ @SuppressWarnings("unchecked") public AutowiredAnnotationBeanPostProcessor() { @@ -206,15 +201,6 @@ public AutowiredAnnotationBeanPostProcessor() { catch (ClassNotFoundException ex) { // jakarta.inject API not available - simply skip. } - - try { - this.autowiredAnnotationTypes.add((Class) - ClassUtils.forName("javax.inject.Inject", classLoader)); - logger.trace("'javax.inject.Inject' annotation found and supported for autowiring"); - } - catch (ClassNotFoundException ex) { - // javax.inject API not available - simply skip. - } } @@ -284,7 +270,7 @@ public void setBeanFactory(BeanFactory beanFactory) { "AutowiredAnnotationBeanPostProcessor requires a ConfigurableListableBeanFactory: " + beanFactory); } this.beanFactory = clbf; - this.metadataReaderFactory = new SimpleMetadataReaderFactory(clbf.getBeanClassLoader()); + this.metadataReaderFactory = MetadataReaderFactory.create(clbf.getBeanClassLoader()); } @@ -312,8 +298,7 @@ public void resetBeanDefinition(String beanName) { } @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); String beanName = registeredBean.getBeanName(); RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); @@ -332,8 +317,7 @@ private Collection getAutowiredElements(InjectionMetadata meta return (Collection) metadata.getInjectedElements(propertyValues); } - @Nullable - private AutowireCandidateResolver getAutowireCandidateResolver() { + private @Nullable AutowireCandidateResolver getAutowireCandidateResolver() { if (this.beanFactory instanceof DefaultListableBeanFactory lbf) { return lbf.getAutowireCandidateResolver(); } @@ -361,8 +345,7 @@ public Class determineBeanType(Class beanClass, String beanName) throws Be } @Override - @Nullable - public Constructor[] determineCandidateConstructors(Class beanClass, final String beanName) + public Constructor @Nullable [] determineCandidateConstructors(Class beanClass, final String beanName) throws BeanCreationException { checkLookupMethods(beanClass, beanName); @@ -623,8 +606,7 @@ private InjectionMetadata buildAutowiringMetadata(Class clazz) { return InjectionMetadata.forElements(elements, clazz); } - @Nullable - private MergedAnnotation findAutowiredAnnotation(AccessibleObject ao) { + private @Nullable MergedAnnotation findAutowiredAnnotation(AccessibleObject ao) { MergedAnnotations annotations = MergedAnnotations.from(ao); for (Class type : this.autowiredAnnotationTypes) { MergedAnnotation annotation = annotations.get(type); @@ -709,8 +691,7 @@ private void registerDependentBeans(@Nullable String beanName, Set autow /** * Resolve the specified cached method argument or field value. */ - @Nullable - private Object resolveCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) { + private @Nullable Object resolveCachedArgument(@Nullable String beanName, @Nullable Object cachedArgument) { if (cachedArgument instanceof DependencyDescriptor descriptor) { Assert.state(this.beanFactory != null, "No BeanFactory available"); return this.beanFactory.resolveDependency(descriptor, beanName, null, null); @@ -742,8 +723,7 @@ private class AutowiredFieldElement extends AutowiredElement { private volatile boolean cached; - @Nullable - private volatile Object cachedFieldValue; + private volatile @Nullable Object cachedFieldValue; public AutowiredFieldElement(Field field, boolean required) { super(field, null, required); @@ -773,8 +753,7 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property } } - @Nullable - private Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) { + private @Nullable Object resolveFieldValue(Field field, Object bean, @Nullable String beanName) { DependencyDescriptor desc = new DependencyDescriptor(field, this.required); desc.setContainingClass(bean.getClass()); Set autowiredBeanNames = new LinkedHashSet<>(2); @@ -820,8 +799,7 @@ private class AutowiredMethodElement extends AutowiredElement { private volatile boolean cached; - @Nullable - private volatile Object[] cachedMethodArguments; + private volatile Object @Nullable [] cachedMethodArguments; public AutowiredMethodElement(Method method, boolean required, @Nullable PropertyDescriptor pd) { super(method, pd, required); @@ -833,7 +811,7 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property return; } Method method = (Method) this.member; - Object[] arguments; + @Nullable Object[] arguments; if (this.cached) { try { arguments = resolveCachedArguments(beanName, this.cachedMethodArguments); @@ -859,22 +837,20 @@ protected void inject(Object bean, @Nullable String beanName, @Nullable Property } } - @Nullable - private Object[] resolveCachedArguments(@Nullable String beanName, @Nullable Object[] cachedMethodArguments) { + private @Nullable Object @Nullable [] resolveCachedArguments(@Nullable String beanName, Object @Nullable [] cachedMethodArguments) { if (cachedMethodArguments == null) { return null; } - Object[] arguments = new Object[cachedMethodArguments.length]; + @Nullable Object[] arguments = new Object[cachedMethodArguments.length]; for (int i = 0; i < arguments.length; i++) { arguments[i] = resolveCachedArgument(beanName, cachedMethodArguments[i]); } return arguments; } - @Nullable - private Object[] resolveMethodArguments(Method method, Object bean, @Nullable String beanName) { + private @Nullable Object @Nullable [] resolveMethodArguments(Method method, Object bean, @Nullable String beanName) { int argumentCount = method.getParameterCount(); - Object[] arguments = new Object[argumentCount]; + @Nullable Object[] arguments = new Object[argumentCount]; DependencyDescriptor[] descriptors = new DependencyDescriptor[argumentCount]; Set autowiredBeanNames = CollectionUtils.newLinkedHashSet(argumentCount); Assert.state(beanFactory != null, "No BeanFactory available"); @@ -960,8 +936,7 @@ private static class AotContribution implements BeanRegistrationAotContribution private final Collection autowiredElements; - @Nullable - private final AutowireCandidateResolver candidateResolver; + private final @Nullable AutowireCandidateResolver candidateResolver; AotContribution(Class target, Collection autowiredElements, @Nullable AutowireCandidateResolver candidateResolver) { @@ -1065,7 +1040,7 @@ private CodeBlock generateMethodStatementForMethod(CodeWarnings codeWarnings, } else { codeWarnings.detectDeprecation(method); - hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); + hints.reflection().registerType(method.getDeclaringClass()); CodeBlock arguments = new AutowiredArgumentsCodeGenerator(this.target, method).generateCode(method.getParameterTypes()); CodeBlock injectionCode = CodeBlock.of("args -> $L.$L($L)", 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 0e1ecd046ff3..df8ad91c32fa 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 @@ -22,6 +22,8 @@ import java.util.Map; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; @@ -34,7 +36,6 @@ import org.springframework.beans.factory.support.AutowireCandidateQualifier; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -146,8 +147,7 @@ else if (beanFactory.containsBean(qualifier) && beanFactory.isTypeMatch(qualifie * @return the associated qualifier value, or {@code null} if none * @since 6.2 */ - @Nullable - public static String getQualifierValue(AnnotatedElement annotatedElement) { + public static @Nullable String getQualifierValue(AnnotatedElement annotatedElement) { Qualifier qualifier = AnnotationUtils.getAnnotation(annotatedElement, Qualifier.class); return (qualifier != null ? qualifier.value() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java index 86fe4482b2ee..32a7b997b1d4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/CustomAutowireConfigurer.java @@ -19,13 +19,14 @@ import java.lang.annotation.Annotation; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -51,11 +52,9 @@ public class CustomAutowireConfigurer implements BeanFactoryPostProcessor, BeanC private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered - @Nullable - private Set customQualifierTypes; + private @Nullable Set customQualifierTypes; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); public void setOrder(int order) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java index 085fe95b0185..79a5b13fcd7b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InitDestroyAnnotationBeanPostProcessor.java @@ -35,6 +35,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; @@ -47,7 +48,6 @@ import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -113,8 +113,7 @@ public boolean hasDestroyMethods() { private int order = Ordered.LOWEST_PRECEDENCE; - @Nullable - private final transient Map, LifecycleMetadata> lifecycleMetadataCache = new ConcurrentHashMap<>(256); + private final transient @Nullable Map, LifecycleMetadata> lifecycleMetadataCache = new ConcurrentHashMap<>(256); /** @@ -183,8 +182,7 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C } @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); beanDefinition.resolveDestroyMethodIfNecessary(); LifecycleMetadata metadata = findLifecycleMetadata(beanDefinition, registeredBean.getBeanClass()); @@ -205,7 +203,7 @@ private LifecycleMetadata findLifecycleMetadata(RootBeanDefinition beanDefinitio return metadata; } - private static String[] safeMerge(@Nullable String[] existingNames, Collection detectedMethods) { + private static String[] safeMerge(String @Nullable [] existingNames, Collection detectedMethods) { Stream detectedNames = detectedMethods.stream().map(LifecycleMethod::getIdentifier); Stream mergedNames = (existingNames != null ? Stream.concat(detectedNames, Stream.of(existingNames)) : detectedNames); @@ -348,11 +346,9 @@ private class LifecycleMetadata { private final Collection destroyMethods; - @Nullable - private volatile Set checkedInitMethods; + private volatile @Nullable Set checkedInitMethods; - @Nullable - private volatile Set checkedDestroyMethods; + private volatile @Nullable Set checkedDestroyMethods; public LifecycleMetadata(Class beanClass, Collection initMethods, Collection destroyMethods) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java index bdd4e4d6a962..1abb51365349 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/InjectionMetadata.java @@ -25,11 +25,12 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -72,8 +73,7 @@ public void clear(@Nullable PropertyValues pvs) { private final Collection injectedElements; - @Nullable - private volatile Set checkedElements; + private volatile @Nullable Set checkedElements; /** @@ -198,11 +198,9 @@ public abstract static class InjectedElement { protected final boolean isField; - @Nullable - protected final PropertyDescriptor pd; + protected final @Nullable PropertyDescriptor pd; - @Nullable - protected volatile Boolean skip; + protected volatile @Nullable Boolean skip; protected InjectedElement(Member member, @Nullable PropertyDescriptor pd) { this.member = member; @@ -335,8 +333,7 @@ protected void clearPropertySkipping(@Nullable PropertyValues pvs) { /** * Either this or {@link #inject} needs to be overridden. */ - @Nullable - protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { + protected @Nullable Object getResourceToInject(Object target, @Nullable String requestingBeanName) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java index 439b1fb30e47..d11c0eab2817 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHints.java @@ -18,10 +18,11 @@ import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} for Jakarta annotations and their pre-Jakarta equivalents. @@ -33,14 +34,10 @@ class JakartaAnnotationsRuntimeHints implements RuntimeHintsRegistrar { @Override public void registerHints(RuntimeHints hints, @Nullable ClassLoader classLoader) { - // javax.inject.Provider is omitted from the list, since we do not currently load - // it via reflection. Stream.of( "jakarta.inject.Inject", "jakarta.inject.Provider", - "jakarta.inject.Qualifier", - "javax.inject.Inject", - "javax.inject.Qualifier" + "jakarta.inject.Qualifier" ).forEach(typeName -> hints.reflection().registerType(TypeReference.of(typeName))); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java index a9dee0daa727..03e253cbd8b1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/ParameterResolutionDelegate.java @@ -22,13 +22,14 @@ import java.lang.reflect.Executable; import java.lang.reflect.Parameter; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.SynthesizingMethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,8 +47,7 @@ public final class ParameterResolutionDelegate { private static final AnnotatedElement EMPTY_ANNOTATED_ELEMENT = new AnnotatedElement() { @Override - @Nullable - public T getAnnotation(Class annotationClass) { + public @Nullable T getAnnotation(Class annotationClass) { return null; } @Override @@ -116,8 +116,7 @@ public static boolean isAutowirable(Parameter parameter, int parameterIndex) { * @see SynthesizingMethodParameter#forExecutable(Executable, int) * @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String) */ - @Nullable - public static Object resolveDependency( + public static @Nullable Object resolveDependency( Parameter parameter, int parameterIndex, Class containingClass, AutowireCapableBeanFactory beanFactory) throws BeansException { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java index 4281e6539497..86fe581d9793 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/QualifierAnnotationAutowireCandidateResolver.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,6 +22,8 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanFactory; @@ -36,7 +38,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -47,8 +48,7 @@ * against {@link Qualifier qualifier annotations} on the field or parameter to be autowired. * Also supports suggested expression values through a {@link Value value} annotation. * - *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation (as well as its - * pre-Jakarta {@code javax.inject.Qualifier} equivalent), if available. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation if available. * * @author Mark Fisher * @author Juergen Hoeller @@ -69,8 +69,7 @@ public class QualifierAnnotationAutowireCandidateResolver extends GenericTypeAwa /** * Create a new {@code QualifierAnnotationAutowireCandidateResolver} for Spring's * standard {@link Qualifier} annotation. - *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation (as well as - * its pre-Jakarta {@code javax.inject.Qualifier} equivalent), if available. + *

Also supports JSR-330's {@link jakarta.inject.Qualifier} annotation if available. */ @SuppressWarnings("unchecked") public QualifierAnnotationAutowireCandidateResolver() { @@ -82,13 +81,6 @@ public QualifierAnnotationAutowireCandidateResolver() { catch (ClassNotFoundException ex) { // JSR-330 API (as included in Jakarta EE) not available - simply skip. } - try { - this.qualifierTypes.add((Class) ClassUtils.forName("javax.inject.Qualifier", - QualifierAnnotationAutowireCandidateResolver.class.getClassLoader())); - } - catch (ClassNotFoundException ex) { - // JSR-330 API not available - simply skip. - } } /** @@ -181,8 +173,7 @@ public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDesc * {@code null} if no qualifier has been found at all */ - @Nullable - protected Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { + protected @Nullable Boolean checkQualifiers(BeanDefinitionHolder bdHolder, Annotation[] annotationsToSearch) { boolean qualifierFound = false; if (!ObjectUtils.isEmpty(annotationsToSearch)) { SimpleTypeConverter typeConverter = new SimpleTypeConverter(); @@ -300,7 +291,7 @@ protected boolean checkQualifier( } } - Map attributes = AnnotationUtils.getAnnotationAttributes(annotation); + Map attributes = AnnotationUtils.getAnnotationAttributes(annotation); if (attributes.isEmpty() && qualifier == null) { // If no attributes, the qualifier must be present return false; @@ -336,14 +327,12 @@ protected boolean checkQualifier( return true; } - @Nullable - protected Annotation getQualifiedElementAnnotation(RootBeanDefinition bd, Class type) { + protected @Nullable Annotation getQualifiedElementAnnotation(RootBeanDefinition bd, Class type) { AnnotatedElement qualifiedElement = bd.getQualifiedElement(); return (qualifiedElement != null ? AnnotationUtils.getAnnotation(qualifiedElement, type) : null); } - @Nullable - protected Annotation getFactoryMethodAnnotation(RootBeanDefinition bd, Class type) { + protected @Nullable Annotation getFactoryMethodAnnotation(RootBeanDefinition bd, Class type) { Method resolvedFactoryMethod = bd.getResolvedFactoryMethod(); return (resolvedFactoryMethod != null ? AnnotationUtils.getAnnotation(resolvedFactoryMethod, type) : null); } @@ -379,8 +368,7 @@ public boolean hasQualifier(DependencyDescriptor descriptor) { } @Override - @Nullable - public String getSuggestedName(DependencyDescriptor descriptor) { + public @Nullable String getSuggestedName(DependencyDescriptor descriptor) { for (Annotation annotation : descriptor.getAnnotations()) { if (isQualifier(annotation.annotationType())) { Object value = AnnotationUtils.getValue(annotation); @@ -397,8 +385,7 @@ public String getSuggestedName(DependencyDescriptor descriptor) { * @see Value */ @Override - @Nullable - public Object getSuggestedValue(DependencyDescriptor descriptor) { + public @Nullable Object getSuggestedValue(DependencyDescriptor descriptor) { Object value = findValue(descriptor.getAnnotations()); if (value == null) { MethodParameter methodParam = descriptor.getMethodParameter(); @@ -412,8 +399,7 @@ public Object getSuggestedValue(DependencyDescriptor descriptor) { /** * Determine a suggested value from any of the given candidate annotations. */ - @Nullable - protected Object findValue(Annotation[] annotationsToSearch) { + protected @Nullable Object findValue(Annotation[] annotationsToSearch) { if (annotationsToSearch.length > 0) { // qualifier annotations have to be local AnnotationAttributes attr = AnnotatedElementUtils.getMergedAnnotationAttributes( AnnotatedElementUtils.forAnnotations(annotationsToSearch), this.valueAnnotationType); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java index 5a277e0126d9..5c96e85a6abc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Support package for annotation-driven bean configuration. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java index 03173bf9a9f7..69d4f79325d4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotBeanProcessingException.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; -import org.springframework.lang.Nullable; /** * Thrown when AOT fails to process a bean. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java index 0b1f810726e5..2bbf63c60b3c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotException.java @@ -16,7 +16,7 @@ package org.springframework.beans.factory.aot; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract superclass for all exceptions thrown by ahead-of-time processing. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java index 87613d63dafb..63762b02682b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotProcessingException.java @@ -16,7 +16,7 @@ package org.springframework.beans.factory.aot; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Throw when an AOT processor failed. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java index c26a9c1ac845..ce30aca0e5b4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AotServices.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,13 +25,14 @@ import java.util.Map; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.support.SpringFactoriesLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -166,8 +167,7 @@ public List asList() { * @param beanName the bean name * @return the AOT service or {@code null} */ - @Nullable - public T findByBeanName(String beanName) { + public @Nullable T findByBeanName(String beanName) { return this.beans.get(beanName); } @@ -191,8 +191,7 @@ public static class Loader { private final SpringFactoriesLoader springFactoriesLoader; - @Nullable - private final ListableBeanFactory beanFactory; + private final @Nullable ListableBeanFactory beanFactory; Loader(SpringFactoriesLoader springFactoriesLoader, @Nullable ListableBeanFactory beanFactory) { @@ -212,9 +211,9 @@ public AotServices load(Class type) { } private Map loadBeans(Class type) { - return (this.beanFactory != null) ? BeanFactoryUtils - .beansOfTypeIncludingAncestors(this.beanFactory, type, true, false) - : Collections.emptyMap(); + return (this.beanFactory != null ? + BeanFactoryUtils.beansOfTypeIncludingAncestors(this.beanFactory, type, true, false) : + Collections.emptyMap()); } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.java index f4c090647919..fc4406811315 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredArguments.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. @@ -16,7 +16,8 @@ package org.springframework.beans.factory.aot; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -40,8 +41,7 @@ public interface AutowiredArguments { * @return the argument */ @SuppressWarnings("unchecked") - @Nullable - default T get(int index, Class requiredType) { + default @Nullable T get(int index, Class requiredType) { Object value = getObject(index); if (!ClassUtils.isAssignableValue(requiredType, value)) { throw new IllegalArgumentException("Argument type mismatch: expected '" + @@ -57,8 +57,7 @@ default T get(int index, Class requiredType) { * @return the argument */ @SuppressWarnings("unchecked") - @Nullable - default T get(int index) { + default @Nullable T get(int index) { return (T) getObject(index); } @@ -67,8 +66,7 @@ default T get(int index) { * @param index the argument index * @return the argument */ - @Nullable - default Object getObject(int index) { + default @Nullable Object getObject(int index) { return toArray()[index]; } @@ -76,7 +74,7 @@ default Object getObject(int index) { * Return the arguments as an object array. * @return the arguments as an object array */ - Object[] toArray(); + @Nullable Object[] toArray(); /** * Factory method to create a new {@link AutowiredArguments} instance from @@ -84,7 +82,7 @@ default Object getObject(int index) { * @param arguments the arguments * @return a new {@link AutowiredArguments} instance */ - static AutowiredArguments of(Object[] arguments) { + static AutowiredArguments of(@Nullable Object[] arguments) { Assert.notNull(arguments, "'arguments' must not be null"); return () -> arguments; } 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 0afc2419a808..c5c863df73aa 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 CodeBlock generateCode(Class[] parameterTypes, int startIndex, String 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/AutowiredFieldValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java index ca3a30e071ba..80ac8fb6dfc9 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredFieldValueResolver.java @@ -20,6 +20,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -29,7 +31,6 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingConsumer; @@ -57,8 +58,7 @@ public final class AutowiredFieldValueResolver extends AutowiredElementResolver private final boolean required; - @Nullable - private final String shortcutBeanName; + private final @Nullable String shortcutBeanName; private AutowiredFieldValueResolver(String fieldName, boolean required, @Nullable String shortcut) { @@ -122,9 +122,8 @@ public void resolve(RegisteredBean registeredBean, ThrowingConsumer actio * @param requiredType the required type * @return the resolved field value */ - @Nullable @SuppressWarnings("unchecked") - public T resolve(RegisteredBean registeredBean, Class requiredType) { + public @Nullable T resolve(RegisteredBean registeredBean, Class requiredType) { Object value = resolveObject(registeredBean); Assert.isInstanceOf(requiredType, value); return (T) value; @@ -135,9 +134,8 @@ public T resolve(RegisteredBean registeredBean, Class requiredType) { * @param registeredBean the registered bean * @return the resolved field value */ - @Nullable @SuppressWarnings("unchecked") - public T resolve(RegisteredBean registeredBean) { + public @Nullable T resolve(RegisteredBean registeredBean) { return (T) resolveObject(registeredBean); } @@ -146,8 +144,7 @@ public T resolve(RegisteredBean registeredBean) { * @param registeredBean the registered bean * @return the resolved field value */ - @Nullable - public Object resolveObject(RegisteredBean registeredBean) { + public @Nullable Object resolveObject(RegisteredBean registeredBean) { Assert.notNull(registeredBean, "'registeredBean' must not be null"); return resolveValue(registeredBean, getField(registeredBean)); } @@ -169,8 +166,7 @@ public void resolveAndSet(RegisteredBean registeredBean, Object instance) { } } - @Nullable - private Object resolveValue(RegisteredBean registeredBean, Field field) { + private @Nullable Object resolveValue(RegisteredBean registeredBean, Field field) { String beanName = registeredBean.getBeanName(); Class beanClass = registeredBean.getBeanClass(); ConfigurableBeanFactory beanFactory = registeredBean.getBeanFactory(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java index e52930dc3de7..62cc5fd51261 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/AutowiredMethodArgumentsResolver.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,8 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -31,7 +33,6 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -62,12 +63,11 @@ public final class AutowiredMethodArgumentsResolver extends AutowiredElementReso private final boolean required; - @Nullable - private final String[] shortcutBeanNames; + private final String @Nullable [] shortcutBeanNames; private AutowiredMethodArgumentsResolver(String methodName, Class[] parameterTypes, - boolean required, @Nullable String[] shortcutBeanNames) { + boolean required, String @Nullable [] shortcutBeanNames) { Assert.hasText(methodName, "'methodName' must not be empty"); this.methodName = methodName; @@ -131,8 +131,7 @@ public void resolve(RegisteredBean registeredBean, ThrowingConsumer autowiredBeanNames = CollectionUtils.newLinkedHashSet(argumentCount); TypeConverter typeConverter = beanFactory.getTypeConverter(); for (int i = 0; i < argumentCount; i++) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java index 074e87df871b..2108261acbce 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGenerator.java @@ -20,6 +20,8 @@ import javax.lang.model.element.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GeneratedMethods; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.javapoet.ClassName; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -46,8 +47,7 @@ class BeanDefinitionMethodGenerator { private final RegisteredBean registeredBean; - @Nullable - private final String currentPropertyName; + private final @Nullable String currentPropertyName; private final List aotContributions; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java index 6938f0c9ef2b..defe9bcbfa6a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactory.java @@ -21,12 +21,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.aot.AotServices.Source; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -69,8 +69,8 @@ class BeanDefinitionMethodGeneratorFactory { this.excludeFilters = loader.load(BeanRegistrationExcludeFilter.class); for (BeanRegistrationExcludeFilter excludeFilter : this.excludeFilters) { if (this.excludeFilters.getSource(excludeFilter) == Source.BEAN_FACTORY) { - Assert.state(excludeFilter instanceof BeanRegistrationAotProcessor - || excludeFilter instanceof BeanFactoryInitializationAotProcessor, + Assert.state(excludeFilter instanceof BeanRegistrationAotProcessor || + excludeFilter instanceof BeanFactoryInitializationAotProcessor, () -> "BeanRegistrationExcludeFilter bean of type %s must also implement an AOT processor interface" .formatted(excludeFilter.getClass().getName())); } @@ -89,8 +89,7 @@ class BeanDefinitionMethodGeneratorFactory { * @param currentPropertyName the property name that this bean belongs to * @return a new {@link BeanDefinitionMethodGenerator} instance or {@code null} */ - @Nullable - BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator( + @Nullable BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator( RegisteredBean registeredBean, @Nullable String currentPropertyName) { if (isExcluded(registeredBean)) { @@ -110,8 +109,7 @@ BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator( * @param registeredBean the registered bean * @return a new {@link BeanDefinitionMethodGenerator} instance or {@code null} */ - @Nullable - BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator(RegisteredBean registeredBean) { + @Nullable BeanDefinitionMethodGenerator getBeanDefinitionMethodGenerator(RegisteredBean registeredBean) { return getBeanDefinitionMethodGenerator(registeredBean, null); } 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 3fb42523b144..e983260eeb03 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 @@ -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,10 +33,11 @@ import java.util.function.Function; import java.util.function.Predicate; +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; @@ -55,7 +56,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -102,15 +102,15 @@ 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 CodeBlock generateCode(RootBeanDefinition beanDefinition) { CodeBlock.Builder code = CodeBlock.builder(); addStatementForValue(code, beanDefinition, BeanDefinition::getScope, @@ -149,7 +149,7 @@ CodeBlock generateCode(RootBeanDefinition beanDefinition) { } private void addInitDestroyMethods(Builder code, AbstractBeanDefinition beanDefinition, - @Nullable String[] methodNames, String format) { + String @Nullable [] methodNames, String format) { // For Publisher-based destroy methods this.hints.reflection().registerType(TypeReference.of("org.reactivestreams.Publisher")); @@ -252,10 +252,10 @@ private void registerReflectionHints(RootBeanDefinition beanDefinition, Method w // ReflectionUtils#findField searches recursively in the type hierarchy Class searchType = beanDefinition.getTargetType(); while (searchType != null && searchType != writeMethod.getDeclaringClass()) { - this.hints.reflection().registerType(searchType, MemberCategory.DECLARED_FIELDS); + this.hints.reflection().registerType(searchType, MemberCategory.ACCESS_DECLARED_FIELDS); searchType = searchType.getSuperclass(); } - this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.DECLARED_FIELDS); + this.hints.reflection().registerType(writeMethod.getDeclaringClass(), MemberCategory.ACCESS_DECLARED_FIELDS); } private void addQualifiers(CodeBlock.Builder code, RootBeanDefinition beanDefinition) { @@ -343,7 +343,7 @@ private Object toRole(int value) { } private void addStatementForValue( - CodeBlock.Builder code, BeanDefinition beanDefinition, Function getter, String format) { + CodeBlock.Builder code, BeanDefinition beanDefinition, Function getter, String format) { addStatementForValue(code, beanDefinition, getter, (defaultValue, actualValue) -> !Objects.equals(defaultValue, actualValue), format); @@ -351,14 +351,14 @@ private void addStatementForValue( private void addStatementForValue( CodeBlock.Builder code, BeanDefinition beanDefinition, - Function getter, BiPredicate filter, String format) { + Function getter, BiPredicate filter, String format) { addStatementForValue(code, beanDefinition, getter, filter, format, actualValue -> actualValue); } - @SuppressWarnings("unchecked") + @SuppressWarnings({"unchecked", "NullAway"}) private void addStatementForValue( - CodeBlock.Builder code, BeanDefinition beanDefinition, Function getter, + CodeBlock.Builder code, BeanDefinition beanDefinition, Function getter, BiPredicate filter, String format, Function formatter) { T defaultValue = getter.apply((B) DEFAULT_BEAN_DEFINITION); @@ -395,8 +395,7 @@ static void pop() { threadLocal.get().pop(); } - @Nullable - static String peek() { + static @Nullable String peek() { String value = threadLocal.get().peek(); return ("".equals(value) ? null : value); } 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 1b9f1fcc8ad5..c1d4108b7a4f 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,12 +16,15 @@ package org.springframework.beans.factory.aot; +import java.util.ArrayList; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Map.Entry; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.ValueCodeGenerator; @@ -37,7 +40,6 @@ import org.springframework.beans.factory.support.ManagedSet; import org.springframework.javapoet.AnnotationSpec; import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; /** * Code generator {@link Delegate} for common bean definition property values. @@ -45,7 +47,7 @@ * @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 @@ -72,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. */ @@ -102,8 +124,7 @@ private static class ManagedMapDelegate implements Delegate { private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.ofEntries()", ManagedMap.class); @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof ManagedMap managedMap) { return generateManagedMapCode(valueCodeGenerator, managedMap); } @@ -139,8 +160,7 @@ private CodeBlock generateManagedMapCode(ValueCodeGenerator valueCodeGene private static class LinkedHashMapDelegate extends MapDelegate { @Override - @Nullable - protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { + protected @Nullable CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { GeneratedMethods generatedMethods = valueCodeGenerator.getGeneratedMethods(); if (map instanceof LinkedHashMap && generatedMethods != null) { return generateLinkedHashMapCode(valueCodeGenerator, generatedMethods, map); @@ -156,6 +176,8 @@ private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerato .builder(SuppressWarnings.class) .addMember("value", "{\"rawtypes\", \"unchecked\"}") .build()); + method.addModifiers(javax.lang.model.element.Modifier.PRIVATE, + javax.lang.model.element.Modifier.STATIC); method.returns(Map.class); method.addStatement("$T map = new $T($L)", Map.class, LinkedHashMap.class, map.size()); @@ -175,8 +197,7 @@ private CodeBlock generateLinkedHashMapCode(ValueCodeGenerator valueCodeGenerato private static class BeanReferenceDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof RuntimeBeanReference runtimeBeanReference && runtimeBeanReference.getBeanType() != null) { return CodeBlock.of("new $T($T.class)", RuntimeBeanReference.class, @@ -197,8 +218,7 @@ else if (value instanceof BeanReference beanReference) { private static class TypedStringValueDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof TypedStringValue typedStringValue) { return generateTypeStringValueCode(valueCodeGenerator, typedStringValue); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java index cfa48d8fc5ae..efb7e4cf5177 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanFactoryInitializationAotProcessor.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; /** * AOT processor that makes bean factory initialization contributions by @@ -58,7 +59,6 @@ public interface BeanFactoryInitializationAotProcessor { * @param beanFactory the bean factory to process * @return a {@link BeanFactoryInitializationAotContribution} or {@code null} */ - @Nullable - BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory); + @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java index 4d6eecec4e36..3f642bd0423b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanInstanceSupplier.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,6 +27,8 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; @@ -43,7 +45,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.support.SimpleInstantiationStrategy; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -85,20 +86,17 @@ public final class BeanInstanceSupplier extends AutowiredElementResolver impl private final ExecutableLookup lookup; - @Nullable - private final ThrowingFunction generatorWithoutArguments; + private final @Nullable ThrowingFunction generatorWithoutArguments; - @Nullable - private final ThrowingBiFunction generatorWithArguments; + private final @Nullable ThrowingBiFunction generatorWithArguments; - @Nullable - private final String[] shortcutBeanNames; + private final String @Nullable [] shortcutBeanNames; private BeanInstanceSupplier(ExecutableLookup lookup, @Nullable ThrowingFunction generatorWithoutArguments, @Nullable ThrowingBiFunction generatorWithArguments, - @Nullable String[] shortcutBeanNames) { + String @Nullable [] shortcutBeanNames) { this.lookup = lookup; this.generatorWithoutArguments = generatorWithoutArguments; @@ -171,30 +169,6 @@ public BeanInstanceSupplier withGenerator(ThrowingFunction return new BeanInstanceSupplier<>(this.lookup, generator, null, this.shortcutBeanNames); } - /** - * Return a new {@link BeanInstanceSupplier} instance that uses the specified - * {@code generator} supplier to instantiate the underlying bean. - * @param generator a {@link ThrowingSupplier} to instantiate the underlying bean - * @return a new {@link BeanInstanceSupplier} instance with the specified generator - * @deprecated in favor of {@link #withGenerator(ThrowingFunction)} - */ - @Deprecated(since = "6.0.11", forRemoval = true) - public BeanInstanceSupplier withGenerator(ThrowingSupplier generator) { - Assert.notNull(generator, "'generator' must not be null"); - return new BeanInstanceSupplier<>(this.lookup, registeredBean -> generator.get(), - null, this.shortcutBeanNames); - } - - /** - * Return a new {@link BeanInstanceSupplier} instance - * that uses direct bean name injection shortcuts for specific parameters. - * @deprecated in favor of {@link #withShortcut(String...)} - */ - @Deprecated(since = "6.2", forRemoval = true) - public BeanInstanceSupplier withShortcuts(String... beanNames) { - return withShortcut(beanNames); - } - /** * Return a new {@link BeanInstanceSupplier} instance that uses * direct bean name injection shortcuts for specific parameters. @@ -226,14 +200,13 @@ else if (this.generatorWithArguments != null) { } else { Executable executable = this.lookup.get(registeredBean); - Object[] arguments = resolveArguments(registeredBean, executable).toArray(); + @Nullable Object[] arguments = resolveArguments(registeredBean, executable).toArray(); return invokeBeanSupplier(executable, () -> (T) instantiate(registeredBean, executable, arguments)); } } @Override - @Nullable - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { // Cached factory method retrieval for qualifier introspection etc. if (this.lookup instanceof FactoryMethodLookup factoryMethodLookup) { return factoryMethodLookup.get(); @@ -241,8 +214,7 @@ public Method getFactoryMethod() { return null; } - @Nullable - private Method getFactoryMethodForGenerator() { + private @Nullable Method getFactoryMethodForGenerator() { // Avoid unnecessary currentlyInvokedFactoryMethod exposure outside of full configuration classes. if (this.lookup instanceof FactoryMethodLookup factoryMethodLookup && factoryMethodLookup.declaringClass.getName().contains(ClassUtils.CGLIB_CLASS_SEPARATOR)) { @@ -270,7 +242,7 @@ AutowiredArguments resolveArguments(RegisteredBean registeredBean) { private AutowiredArguments resolveArguments(RegisteredBean registeredBean, Executable executable) { int parameterCount = executable.getParameterCount(); - Object[] resolved = new Object[parameterCount]; + @Nullable Object[] resolved = new Object[parameterCount]; Assert.isTrue(this.shortcutBeanNames == null || this.shortcutBeanNames.length == resolved.length, () -> "'shortcuts' must contain " + resolved.length + " elements"); @@ -352,8 +324,7 @@ private ValueHolder resolveArgumentValue(BeanDefinitionValueResolver resolver, V return resolvedHolder; } - @Nullable - private Object resolveAutowiredArgument(RegisteredBean registeredBean, DependencyDescriptor descriptor, + private @Nullable Object resolveAutowiredArgument(RegisteredBean registeredBean, DependencyDescriptor descriptor, @Nullable ValueHolder argumentValue, Set autowiredBeanNames) { TypeConverter typeConverter = registeredBean.getBeanFactory().getTypeConverter(); @@ -370,7 +341,7 @@ private Object resolveAutowiredArgument(RegisteredBean registeredBean, Dependenc } } - private Object instantiate(RegisteredBean registeredBean, Executable executable, Object[] args) { + private Object instantiate(RegisteredBean registeredBean, Executable executable, @Nullable Object[] args) { if (executable instanceof Constructor constructor) { return BeanUtils.instantiateClass(constructor, args); } @@ -450,8 +421,7 @@ private static class FactoryMethodLookup extends ExecutableLookup { private final Class[] parameterTypes; - @Nullable - private volatile Method resolvedMethod; + private volatile @Nullable Method resolvedMethod; FactoryMethodLookup(Class declaringClass, String methodName, Class[] parameterTypes) { this.declaringClass = declaringClass; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java index 42a16c15238a..da4c5b7a6aae 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotContribution.java @@ -18,8 +18,9 @@ import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -94,8 +95,7 @@ public void applyTo(GenerationContext generationContext, * they are both {@code null}. * @since 6.1 */ - @Nullable - static BeanRegistrationAotContribution concat(@Nullable BeanRegistrationAotContribution a, + static @Nullable BeanRegistrationAotContribution concat(@Nullable BeanRegistrationAotContribution a, @Nullable BeanRegistrationAotContribution b) { if (a == null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java index 30a57779a2c8..b1e65c238d75 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationAotProcessor.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.lang.Nullable; /** * AOT processor that makes bean registration contributions by processing @@ -72,8 +73,7 @@ public interface BeanRegistrationAotProcessor { * @param registeredBean the registered bean to process * @return a {@link BeanRegistrationAotContribution} or {@code null} */ - @Nullable - BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean); + @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean); /** * Return if the bean instance associated with this processor should be diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java index ddb3ba60cdf6..67ac2db28744 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContribution.java @@ -26,9 +26,9 @@ import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeHint; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.javapoet.ClassName; @@ -100,8 +100,8 @@ private void generateRegisterHints(RuntimeHints runtimeHints, List registrations.forEach(registration -> { ReflectionHints hints = runtimeHints.reflection(); Class beanClass = registration.registeredBean.getBeanClass(); - hints.registerType(beanClass, MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS); - hints.registerForInterfaces(beanClass, typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + hints.registerType(beanClass); + hints.registerForInterfaces(beanClass, TypeHint.Builder::withMembers); }); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java index 183ad41af15c..9f4491c25d5b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/BeanRegistrationsAotProcessor.java @@ -19,10 +19,11 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; -import org.springframework.lang.Nullable; /** * {@link BeanFactoryInitializationAotProcessor} that contributes code to @@ -37,8 +38,7 @@ class BeanRegistrationsAotProcessor implements BeanFactoryInitializationAotProcessor { @Override - @Nullable - public BeanRegistrationsAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + public @Nullable BeanRegistrationsAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { BeanDefinitionMethodGeneratorFactory beanDefinitionMethodGeneratorFactory = new BeanDefinitionMethodGeneratorFactory(beanFactory); List registrations = new ArrayList<>(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java index 06e5eb0d33a0..db504c6b8b8e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/CodeWarnings.java @@ -23,6 +23,8 @@ import java.util.function.Consumer; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.javapoet.AnnotationSpec; import org.springframework.javapoet.AnnotationSpec.Builder; @@ -30,7 +32,6 @@ import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java index 498b04633b41..48ec28face44 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragments.java @@ -22,6 +22,8 @@ import java.util.function.Predicate; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; @@ -40,7 +42,6 @@ import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.function.SingletonSupplier; @@ -176,8 +177,7 @@ public CodeBlock generateSetBeanDefinitionPropertiesCode( .generateCode(beanDefinition); } - @Nullable - protected CodeBlock generateValueCode(GenerationContext generationContext, String name, Object value) { + protected @Nullable CodeBlock generateValueCode(GenerationContext generationContext, String name, Object value) { RegisteredBean innerRegisteredBean = getInnerRegisteredBean(value); if (innerRegisteredBean != null) { BeanDefinitionMethodGenerator methodGenerator = this.beanDefinitionMethodGeneratorFactory @@ -190,8 +190,7 @@ protected CodeBlock generateValueCode(GenerationContext generationContext, Strin return null; } - @Nullable - private RegisteredBean getInnerRegisteredBean(Object value) { + private @Nullable RegisteredBean getInnerRegisteredBean(Object value) { if (value instanceof BeanDefinitionHolder beanDefinitionHolder) { return RegisteredBean.ofInnerBean(this.registeredBean, beanDefinitionHolder); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java index 3e03043075c3..a2fb11b9b60e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/InstanceSupplierCodeGenerator.java @@ -29,6 +29,7 @@ import kotlin.reflect.KClass; import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.AccessControl; import org.springframework.aot.generate.AccessControl.Visibility; @@ -54,7 +55,6 @@ import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.function.ThrowingSupplier; @@ -160,7 +160,7 @@ private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Cons registeredBean.getBeanName(), constructor, registeredBean.getBeanClass()); Class publicType = descriptor.publicType(); - if (KotlinDetector.isKotlinReflectPresent() && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) { + if (KotlinDetector.isKotlinType(publicType) && KotlinDelegate.hasConstructorWithOptionalParameter(publicType)) { return generateCodeForInaccessibleConstructor(descriptor, hints -> hints.registerType(publicType, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)); } @@ -174,8 +174,7 @@ private CodeBlock generateCodeForConstructor(RegisteredBean registeredBean, Cons private CodeBlock generateCodeForAccessibleConstructor(ConstructorDescriptor descriptor) { Constructor constructor = descriptor.constructor(); - this.generationContext.getRuntimeHints().reflection().registerConstructor( - constructor, ExecutableMode.INTROSPECT); + this.generationContext.getRuntimeHints().reflection().registerType(constructor.getDeclaringClass()); if (constructor.getParameterCount() == 0) { if (!this.allowDirectSupplierShortcut) { @@ -234,8 +233,7 @@ private void buildGetInstanceMethodForConstructor(MethodSpec.Builder method, Con CodeBlock arguments = hasArguments ? new AutowiredArgumentsCodeGenerator(actualType, constructor) - .generateCode(constructor.getParameterTypes(), (onInnerClass ? 1 : 0)) - : NO_ARGS; + .generateCode(constructor.getParameterTypes(), (onInnerClass ? 1 : 0)) : NO_ARGS; CodeBlock newInstance = generateNewInstanceCodeForConstructor(actualType, arguments); code.add(generateWithGeneratorCode(hasArguments, newInstance)); @@ -270,7 +268,7 @@ private CodeBlock generateCodeForFactoryMethod( private CodeBlock generateCodeForAccessibleFactoryMethod(String beanName, Method factoryMethod, Class targetClass, @Nullable String factoryBeanName) { - this.generationContext.getRuntimeHints().reflection().registerMethod(factoryMethod, ExecutableMode.INTROSPECT); + this.generationContext.getRuntimeHints().reflection().registerType(factoryMethod.getDeclaringClass()); if (factoryBeanName == null && factoryMethod.getParameterCount() == 0) { Class suppliedType = ClassUtils.resolvePrimitiveIfNecessary(factoryMethod.getReturnType()); @@ -325,8 +323,7 @@ private void buildGetInstanceMethodForFactoryMethod(MethodSpec.Builder method, boolean hasArguments = factoryMethod.getParameterCount() > 0; CodeBlock arguments = hasArguments ? new AutowiredArgumentsCodeGenerator(ClassUtils.getUserClass(targetClass), factoryMethod) - .generateCode(factoryMethod.getParameterTypes()) - : NO_ARGS; + .generateCode(factoryMethod.getParameterTypes()) : NO_ARGS; CodeBlock newInstance = generateNewInstanceCodeForMethod( factoryBeanName, ClassUtils.getUserClass(targetClass), factoryMethodName, arguments); @@ -409,13 +406,11 @@ private boolean isThrowingCheckedException(Executable executable) { private static class KotlinDelegate { public static boolean hasConstructorWithOptionalParameter(Class beanClass) { - if (KotlinDetector.isKotlinType(beanClass)) { - KClass kClass = JvmClassMappingKt.getKotlinClass(beanClass); - for (KFunction constructor : kClass.getConstructors()) { - for (KParameter parameter : constructor.getParameters()) { - if (parameter.isOptional()) { - return true; - } + KClass kClass = JvmClassMappingKt.getKotlinClass(beanClass); + for (KFunction constructor : kClass.getConstructors()) { + for (KParameter parameter : constructor.getParameters()) { + if (parameter.isOptional()) { + return true; } } } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java index bf7c97a915d0..41631ad6a3b6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/aot/package-info.java @@ -1,9 +1,7 @@ /** * AOT support for bean factories. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.aot; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/AbstractFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AbstractFactoryBean.java index ea7958d4d53f..b6a2417cef0a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/AbstractFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/AbstractFactoryBean.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; @@ -33,7 +34,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -68,19 +68,16 @@ public abstract class AbstractFactoryBean private boolean singleton = true; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; private boolean initialized = false; - @Nullable + @SuppressWarnings("NullAway.Init") private T singletonInstance; - @Nullable - private T earlySingletonInstance; + private @Nullable T earlySingletonInstance; /** @@ -109,8 +106,7 @@ public void setBeanFactory(@Nullable BeanFactory beanFactory) { /** * Return the BeanFactory that this bean runs in. */ - @Nullable - protected BeanFactory getBeanFactory() { + protected @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } @@ -151,7 +147,6 @@ public void afterPropertiesSet() throws Exception { * @see #getEarlySingletonInterfaces() */ @Override - @SuppressWarnings("NullAway") public final T getObject() throws Exception { if (isSingleton()) { return (this.initialized ? this.singletonInstance : getEarlySingletonInstance()); @@ -184,8 +179,7 @@ private T getEarlySingletonInstance() throws Exception { * @return the singleton instance that this FactoryBean holds * @throws IllegalStateException if the singleton instance is not initialized */ - @Nullable - private T getSingletonInstance() throws IllegalStateException { + private @Nullable T getSingletonInstance() throws IllegalStateException { Assert.state(this.initialized, "Singleton instance not initialized yet"); return this.singletonInstance; } @@ -208,8 +202,7 @@ public void destroy() throws Exception { * @see org.springframework.beans.factory.FactoryBean#getObjectType() */ @Override - @Nullable - public abstract Class getObjectType(); + public abstract @Nullable Class getObjectType(); /** * Template method that subclasses must override to construct @@ -234,8 +227,7 @@ public void destroy() throws Exception { * or {@code null} to indicate a FactoryBeanNotInitializedException * @see org.springframework.beans.factory.FactoryBeanNotInitializedException */ - @Nullable - protected Class[] getEarlySingletonInterfaces() { + protected Class @Nullable [] getEarlySingletonInterfaces() { Class type = getObjectType(); return (type != null && type.isInterface() ? new Class[] {type} : null); } 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 cd7044c89f46..48ba5e61eac0 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. @@ -18,12 +18,13 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.NoUniqueBeanDefinitionException; -import org.springframework.lang.Nullable; /** * Extension of the {@link org.springframework.beans.factory.BeanFactory} @@ -96,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; /** @@ -187,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; @@ -381,8 +382,7 @@ Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String be * @since 2.5 * @see #resolveDependency(DependencyDescriptor, String, Set, TypeConverter) */ - @Nullable - Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException; + @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException; /** * Resolve the specified dependency against the beans defined in this factory. @@ -398,8 +398,7 @@ Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String be * @since 2.5 * @see DependencyDescriptor */ - @Nullable - Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, + @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java index 7457494436df..27771db8ebbd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/AutowiredPropertyMarker.java @@ -18,7 +18,7 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple marker class for an individually autowired property value, to be added diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java index 0f6f6ab5cb66..f581179a8cfb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinition.java @@ -16,11 +16,12 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.MutablePropertyValues; import org.springframework.core.AttributeAccessor; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * A BeanDefinition describes a bean instance, which has property values, @@ -93,8 +94,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { /** * Return the name of the parent definition of this bean definition, if any. */ - @Nullable - String getParentName(); + @Nullable String getParentName(); /** * Specify the bean class name of this bean definition. @@ -118,8 +118,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * @see #getFactoryBeanName() * @see #getFactoryMethodName() */ - @Nullable - String getBeanClassName(); + @Nullable String getBeanClassName(); /** * Override the target scope of this bean, specifying a new scope name. @@ -132,8 +131,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * Return the name of the current target scope for this bean, * or {@code null} if not known yet. */ - @Nullable - String getScope(); + @Nullable String getScope(); /** * Set whether this bean should be lazily initialized. @@ -155,13 +153,12 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * constructor arguments. This property should just be necessary for other kinds * of dependencies like statics (*ugh*) or database preparation on startup. */ - void setDependsOn(@Nullable String... dependsOn); + void setDependsOn(String @Nullable ... dependsOn); /** * Return the bean names that this bean depends on. */ - @Nullable - String[] getDependsOn(); + String @Nullable [] getDependsOn(); /** * Set whether this bean is a candidate for getting autowired into some other bean. @@ -222,8 +219,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * @see #getFactoryMethodName() * @see #getBeanClassName() */ - @Nullable - String getFactoryBeanName(); + @Nullable String getFactoryBeanName(); /** * Specify a factory method, if any. This method will be invoked with @@ -240,8 +236,7 @@ public interface BeanDefinition extends AttributeAccessor, BeanMetadataElement { * @see #getFactoryBeanName() * @see #getBeanClassName() */ - @Nullable - String getFactoryMethodName(); + @Nullable String getFactoryMethodName(); /** * Return the constructor argument values for this bean. @@ -285,8 +280,7 @@ default boolean hasPropertyValues() { * Return the name of the initializer method. * @since 5.1 */ - @Nullable - String getInitMethodName(); + @Nullable String getInitMethodName(); /** * Set the name of the destroy method. @@ -298,8 +292,7 @@ default boolean hasPropertyValues() { * Return the name of the destroy method. * @since 5.1 */ - @Nullable - String getDestroyMethodName(); + @Nullable String getDestroyMethodName(); /** * Set the role hint for this {@code BeanDefinition}. The role hint @@ -331,8 +324,7 @@ default boolean hasPropertyValues() { /** * Return a human-readable description of this bean definition. */ - @Nullable - String getDescription(); + @Nullable String getDescription(); // Read-only attributes @@ -373,8 +365,7 @@ default boolean hasPropertyValues() { * Return a description of the resource that this bean definition * came from (for the purpose of showing context in case of errors). */ - @Nullable - String getResourceDescription(); + @Nullable String getResourceDescription(); /** * Return the originating BeanDefinition, or {@code null} if none. @@ -382,7 +373,6 @@ default boolean hasPropertyValues() { *

Note that this method returns the immediate originator. Iterate through the * originator chain to find the original BeanDefinition as defined by the user. */ - @Nullable - BeanDefinition getOriginatingBeanDefinition(); + @Nullable BeanDefinition getOriginatingBeanDefinition(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java index 9b76f819fce2..eded121a8c2d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionHolder.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.BeanFactoryUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -43,8 +44,7 @@ public class BeanDefinitionHolder implements BeanMetadataElement { private final String beanName; - @Nullable - private final String[] aliases; + private final String @Nullable [] aliases; /** @@ -62,7 +62,7 @@ public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName) { * @param beanName the name of the bean, as specified for the bean definition * @param aliases alias names for the bean, or {@code null} if none */ - public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName, @Nullable String[] aliases) { + public BeanDefinitionHolder(BeanDefinition beanDefinition, String beanName, String @Nullable [] aliases) { Assert.notNull(beanDefinition, "BeanDefinition must not be null"); Assert.notNull(beanName, "Bean name must not be null"); this.beanDefinition = beanDefinition; @@ -103,8 +103,7 @@ public String getBeanName() { * Return the alias names for the bean, as specified directly for the bean definition. * @return the array of alias names, or {@code null} if none */ - @Nullable - public String[] getAliases() { + public String @Nullable [] getAliases() { return this.aliases; } @@ -113,8 +112,7 @@ public String[] getAliases() { * @see BeanDefinition#getSource() */ @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.beanDefinition.getSource(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java index 878735ec1a2a..96c03de1ef9a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanDefinitionVisitor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,9 +22,10 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringValueResolver; @@ -47,8 +48,7 @@ */ public class BeanDefinitionVisitor { - @Nullable - private StringValueResolver valueResolver; + private @Nullable StringValueResolver valueResolver; /** @@ -170,8 +170,7 @@ protected void visitGenericArgumentValues(List mapVal) { * @param strVal the original String value * @return the resolved String value */ - @Nullable - protected String resolveStringValue(String strVal) { + protected @Nullable String resolveStringValue(String strVal) { if (this.valueResolver == null) { throw new IllegalStateException("No StringValueResolver specified - pass a resolver " + "object into the constructor or override the 'resolveStringValue' method"); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java index 7fa5b36b07d8..0b5eb79a6462 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionContext.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -29,8 +30,7 @@ public class BeanExpressionContext { private final ConfigurableBeanFactory beanFactory; - @Nullable - private final Scope scope; + private final @Nullable Scope scope; public BeanExpressionContext(ConfigurableBeanFactory beanFactory, @Nullable Scope scope) { @@ -43,8 +43,7 @@ public final ConfigurableBeanFactory getBeanFactory() { return this.beanFactory; } - @Nullable - public final Scope getScope() { + public final @Nullable Scope getScope() { return this.scope; } @@ -54,8 +53,7 @@ public boolean containsObject(String key) { (this.scope != null && this.scope.resolveContextualObject(key) != null)); } - @Nullable - public Object getObject(String key) { + public @Nullable Object getObject(String key) { if (this.beanFactory.containsBean(key)) { return this.beanFactory.getBean(key); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java index 2975de790c4c..30159c400a44 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanExpressionResolver.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; /** * Strategy interface for resolving a value by evaluating it as an expression, @@ -42,7 +43,6 @@ public interface BeanExpressionResolver { * @return the resolved value (potentially the given value as-is) * @throws BeansException if evaluation failed */ - @Nullable - Object evaluate(@Nullable String value, BeanExpressionContext beanExpressionContext) throws BeansException; + @Nullable Object evaluate(@Nullable String value, BeanExpressionContext beanExpressionContext) throws BeansException; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java index 7288aa476cf4..5aa701f19331 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/BeanPostProcessor.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; /** * Factory hook that allows for custom modification of new bean instances — @@ -70,8 +71,7 @@ public interface BeanPostProcessor { * @throws org.springframework.beans.BeansException in case of errors * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet */ - @Nullable - default Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + default @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { return bean; } @@ -96,8 +96,7 @@ default Object postProcessBeforeInitialization(Object bean, String beanName) thr * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet * @see org.springframework.beans.factory.FactoryBean */ - @Nullable - default Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { + default @Nullable Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { return bean; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java index e4fb00877b59..f254ebc49d4e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableBeanFactory.java @@ -19,6 +19,8 @@ import java.beans.PropertyEditor; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.TypeConverter; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.metrics.ApplicationStartup; -import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; /** @@ -94,8 +95,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * (only {@code null} if even the system ClassLoader isn't accessible). * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) */ - @Nullable - ClassLoader getBeanClassLoader(); + @Nullable ClassLoader getBeanClassLoader(); /** * Specify a temporary ClassLoader to use for type matching purposes. @@ -113,8 +113,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * if any. * @since 2.5 */ - @Nullable - ClassLoader getTempClassLoader(); + @Nullable ClassLoader getTempClassLoader(); /** * Set whether to cache bean metadata such as given bean definitions @@ -144,8 +143,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * Return the resolution strategy for expressions in bean definition values. * @since 3.0 */ - @Nullable - BeanExpressionResolver getBeanExpressionResolver(); + @Nullable BeanExpressionResolver getBeanExpressionResolver(); /** * Set the {@link Executor} (possibly a {@link org.springframework.core.task.TaskExecutor}) @@ -160,8 +158,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * for background bootstrapping, if any. * @since 6.2 */ - @Nullable - Executor getBootstrapExecutor(); + @Nullable Executor getBootstrapExecutor(); /** * Specify a {@link ConversionService} to use for converting @@ -174,8 +171,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * Return the associated ConversionService, if any. * @since 3.0 */ - @Nullable - ConversionService getConversionService(); + @Nullable ConversionService getConversionService(); /** * Add a PropertyEditorRegistrar to be applied to all bean creation processes. @@ -250,8 +246,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * @return the resolved value (may be the original value as-is) * @since 3.0 */ - @Nullable - String resolveEmbeddedValue(String value); + @Nullable String resolveEmbeddedValue(String value); /** * Add a new BeanPostProcessor that will get applied to beans created @@ -294,8 +289,7 @@ public interface ConfigurableBeanFactory extends HierarchicalBeanFactory, Single * @return the registered Scope implementation, or {@code null} if none * @see #registerScope */ - @Nullable - Scope getRegisteredScope(String scopeName); + @Nullable Scope getRegisteredScope(String scopeName); /** * Set the {@code ApplicationStartup} for this bean factory. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java index fe451841ab42..c17bb52e999e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConfigurableListableBeanFactory.java @@ -18,10 +18,11 @@ import java.util.Iterator; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; -import org.springframework.lang.Nullable; /** * Configuration interface to be implemented by most listable bean factories. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java index 175f5a4c0ba6..09579f00fb8a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ConstructorArgumentValues.java @@ -24,9 +24,10 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.Mergeable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -144,8 +145,7 @@ public boolean hasIndexedArgumentValue(int index) { * untyped values only) * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getIndexedArgumentValue(int index, @Nullable Class requiredType) { + public @Nullable ValueHolder getIndexedArgumentValue(int index, @Nullable Class requiredType) { return getIndexedArgumentValue(index, requiredType, null); } @@ -158,8 +158,7 @@ public ValueHolder getIndexedArgumentValue(int index, @Nullable Class require * unnamed values only, or empty String to match any name) * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getIndexedArgumentValue(int index, @Nullable Class requiredType, @Nullable String requiredName) { + public @Nullable ValueHolder getIndexedArgumentValue(int index, @Nullable Class requiredType, @Nullable String requiredName) { Assert.isTrue(index >= 0, "Index must not be negative"); ValueHolder valueHolder = this.indexedArgumentValues.get(index); if (valueHolder != null && @@ -246,8 +245,7 @@ private void addOrMergeGenericArgumentValue(ValueHolder newValue) { * @param requiredType the type to match * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getGenericArgumentValue(Class requiredType) { + public @Nullable ValueHolder getGenericArgumentValue(Class requiredType) { return getGenericArgumentValue(requiredType, null, null); } @@ -257,8 +255,7 @@ public ValueHolder getGenericArgumentValue(Class requiredType) { * @param requiredName the name to match * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getGenericArgumentValue(Class requiredType, String requiredName) { + public @Nullable ValueHolder getGenericArgumentValue(Class requiredType, String requiredName) { return getGenericArgumentValue(requiredType, requiredName, null); } @@ -274,8 +271,7 @@ public ValueHolder getGenericArgumentValue(Class requiredType, String require * in the current resolution process and should therefore not be returned again * @return the ValueHolder for the argument, or {@code null} if none found */ - @Nullable - public ValueHolder getGenericArgumentValue(@Nullable Class requiredType, @Nullable String requiredName, + public @Nullable ValueHolder getGenericArgumentValue(@Nullable Class requiredType, @Nullable String requiredName, @Nullable Set usedValueHolders) { for (ValueHolder valueHolder : this.genericArgumentValues) { @@ -316,8 +312,7 @@ public List getGenericArgumentValues() { * @param requiredType the parameter type to match * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getArgumentValue(int index, Class requiredType) { + public @Nullable ValueHolder getArgumentValue(int index, Class requiredType) { return getArgumentValue(index, requiredType, null, null); } @@ -329,8 +324,7 @@ public ValueHolder getArgumentValue(int index, Class requiredType) { * @param requiredName the parameter name to match * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getArgumentValue(int index, Class requiredType, String requiredName) { + public @Nullable ValueHolder getArgumentValue(int index, Class requiredType, String requiredName) { return getArgumentValue(index, requiredType, requiredName, null); } @@ -348,8 +342,7 @@ public ValueHolder getArgumentValue(int index, Class requiredType, String req * in case of multiple generic argument values of the same type) * @return the ValueHolder for the argument, or {@code null} if none set */ - @Nullable - public ValueHolder getArgumentValue(int index, @Nullable Class requiredType, + public @Nullable ValueHolder getArgumentValue(int index, @Nullable Class requiredType, @Nullable String requiredName, @Nullable Set usedValueHolders) { Assert.isTrue(index >= 0, "Index must not be negative"); @@ -455,22 +448,17 @@ public int hashCode() { */ public static class ValueHolder implements BeanMetadataElement { - @Nullable - private Object value; + private @Nullable Object value; - @Nullable - private String type; + private @Nullable String type; - @Nullable - private String name; + private @Nullable String name; - @Nullable - private Object source; + private @Nullable Object source; private boolean converted = false; - @Nullable - private Object convertedValue; + private @Nullable Object convertedValue; /** * Create a new ValueHolder for the given value. @@ -512,8 +500,7 @@ public void setValue(@Nullable Object value) { /** * Return the value for the constructor argument. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } @@ -527,8 +514,7 @@ public void setType(@Nullable String type) { /** * Return the type of the constructor argument. */ - @Nullable - public String getType() { + public @Nullable String getType() { return this.type; } @@ -542,8 +528,7 @@ public void setName(@Nullable String name) { /** * Return the name of the constructor argument. */ - @Nullable - public String getName() { + public @Nullable String getName() { return this.name; } @@ -556,8 +541,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -582,8 +566,7 @@ public synchronized void setConvertedValue(@Nullable Object value) { * Return the converted value of the constructor argument, * after processed type conversion. */ - @Nullable - public synchronized Object getConvertedValue() { + public synchronized @Nullable Object getConvertedValue() { return this.convertedValue; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java index 64963ead5435..2d3dd8a5bb71 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomEditorConfigurer.java @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.PropertyEditorRegistrar; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -99,11 +99,9 @@ public class CustomEditorConfigurer implements BeanFactoryPostProcessor, Ordered private int order = Ordered.LOWEST_PRECEDENCE; // default: same as non-Ordered - @Nullable - private PropertyEditorRegistrar[] propertyEditorRegistrars; + private PropertyEditorRegistrar @Nullable [] propertyEditorRegistrars; - @Nullable - private Map, Class> customEditors; + private @Nullable Map, Class> customEditors; public void setOrder(int order) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java index 8bf43ae269a7..901ad062c283 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/CustomScopeConfigurer.java @@ -19,11 +19,12 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -46,13 +47,11 @@ */ public class CustomScopeConfigurer implements BeanFactoryPostProcessor, BeanClassLoaderAware, Ordered { - @Nullable - private Map scopes; + private @Nullable Map scopes; private int order = Ordered.LOWEST_PRECEDENCE; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** 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 e7dc1a602a5e..884d87189342 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 @@ -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,24 +19,21 @@ 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; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -52,16 +49,13 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable private final Class declaringClass; - @Nullable - private String methodName; + private @Nullable String methodName; - @Nullable - private Class[] parameterTypes; + private Class @Nullable [] parameterTypes; private int parameterIndex; - @Nullable - private String fieldName; + private @Nullable String fieldName; private final boolean required; @@ -69,14 +63,11 @@ public class DependencyDescriptor extends InjectionPoint implements Serializable private int nestingLevel = 1; - @Nullable - private Class containingClass; + private @Nullable Class containingClass; - @Nullable - private transient volatile ResolvableType resolvableType; + private transient volatile @Nullable ResolvableType resolvableType; - @Nullable - private transient volatile TypeDescriptor typeDescriptor; + private transient volatile @Nullable TypeDescriptor typeDescriptor; /** @@ -168,30 +159,13 @@ public boolean isRequired() { } if (this.field != null) { - return !(this.field.getType() == Optional.class || hasNullableAnnotation() || - (KotlinDetector.isKotlinReflectPresent() && - 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. @@ -213,8 +187,7 @@ public boolean isEager() { * @throws BeansException in case of the not-unique scenario being fatal * @since 5.1 */ - @Nullable - public Object resolveNotUnique(ResolvableType type, Map matchingBeans) throws BeansException { + public @Nullable Object resolveNotUnique(ResolvableType type, Map matchingBeans) throws BeansException { throw new NoUniqueBeanDefinitionException(type, matchingBeans.keySet()); } @@ -230,8 +203,7 @@ public Object resolveNotUnique(ResolvableType type, Map matching * @throws BeansException if the shortcut could not be obtained * @since 4.3.1 */ - @Nullable - public Object resolveShortcut(BeanFactory beanFactory) throws BeansException { + public @Nullable Object resolveShortcut(BeanFactory beanFactory) throws BeansException { return null; } @@ -355,8 +327,7 @@ public void initParameterNameDiscovery(@Nullable ParameterNameDiscoverer paramet * Determine the name of the wrapped parameter/field. * @return the declared name (may be {@code null} if unresolvable) */ - @Nullable - public String getDependencyName() { + public @Nullable String getDependencyName() { return (this.field != null ? this.field.getName() : obtainMethodParameter().getParameterName()); } @@ -456,19 +427,4 @@ private void readObject(ObjectInputStream ois) throws IOException, ClassNotFound } } - - /** - * 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/EmbeddedValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/EmbeddedValueResolver.java index f38156bcb950..ea2bf5b8abaa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/EmbeddedValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/EmbeddedValueResolver.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringValueResolver; /** @@ -38,8 +39,7 @@ public class EmbeddedValueResolver implements StringValueResolver { private final BeanExpressionContext exprContext; - @Nullable - private final BeanExpressionResolver exprResolver; + private final @Nullable BeanExpressionResolver exprResolver; public EmbeddedValueResolver(ConfigurableBeanFactory beanFactory) { @@ -49,8 +49,7 @@ public EmbeddedValueResolver(ConfigurableBeanFactory beanFactory) { @Override - @Nullable - public String resolveStringValue(String strVal) { + public @Nullable String resolveStringValue(String strVal) { String value = this.exprContext.getBeanFactory().resolveEmbeddedValue(strVal); if (this.exprResolver != null && value != null) { Object evaluated = this.exprResolver.evaluate(value, this.exprContext); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java index 6c132145b3bd..651699904d58 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/FieldRetrievingFactoryBean.java @@ -18,13 +18,14 @@ import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -58,27 +59,20 @@ public class FieldRetrievingFactoryBean implements FactoryBean, BeanNameAware, BeanClassLoaderAware, InitializingBean { - @Nullable - private Class targetClass; + private @Nullable Class targetClass; - @Nullable - private Object targetObject; + private @Nullable Object targetObject; - @Nullable - private String targetField; + private @Nullable String targetField; - @Nullable - private String staticField; + private @Nullable String staticField; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); // the field we will retrieve - @Nullable - private Field fieldObject; + private @Nullable Field fieldObject; /** @@ -95,8 +89,7 @@ public void setTargetClass(@Nullable Class targetClass) { /** * Return the target class on which the field is defined. */ - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetClass; } @@ -114,8 +107,7 @@ public void setTargetObject(@Nullable Object targetObject) { /** * Return the target object on which the field is defined. */ - @Nullable - public Object getTargetObject() { + public @Nullable Object getTargetObject() { return this.targetObject; } @@ -133,8 +125,7 @@ public void setTargetField(@Nullable String targetField) { /** * Return the name of the field to be retrieved. */ - @Nullable - public String getTargetField() { + public @Nullable String getTargetField() { return this.targetField; } @@ -167,7 +158,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void afterPropertiesSet() throws ClassNotFoundException, NoSuchFieldException { if (this.targetClass != null && this.targetObject != null) { throw new IllegalArgumentException("Specify either targetClass or targetObject, not both"); @@ -210,8 +201,7 @@ else if (this.targetField == null) { @Override - @Nullable - public Object getObject() throws IllegalAccessException { + public @Nullable Object getObject() throws IllegalAccessException { if (this.fieldObject == null) { throw new FactoryBeanNotInitializedException(); } @@ -227,8 +217,7 @@ public Object getObject() throws IllegalAccessException { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return (this.fieldObject != null ? this.fieldObject.getType() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java index e842353cb55d..c00cb529b21d 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/InstantiationAwareBeanPostProcessor.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.PropertyValues; -import org.springframework.lang.Nullable; /** * Subinterface of {@link BeanPostProcessor} that adds a before-instantiation callback, @@ -66,8 +67,7 @@ public interface InstantiationAwareBeanPostProcessor extends BeanPostProcessor { * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getBeanClass() * @see org.springframework.beans.factory.support.AbstractBeanDefinition#getFactoryMethodName() */ - @Nullable - default Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { + default @Nullable Object postProcessBeforeInstantiation(Class beanClass, String beanName) throws BeansException { return null; } @@ -102,8 +102,7 @@ default boolean postProcessAfterInstantiation(Object bean, String beanName) thro * @throws org.springframework.beans.BeansException in case of errors * @since 5.1 */ - @Nullable - default PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) + default @Nullable PropertyValues postProcessProperties(PropertyValues pvs, Object bean, String beanName) throws BeansException { return pvs; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java index d9b89210f08d..b73a9f9a8eb5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ListFactoryBean.java @@ -19,10 +19,11 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * Simple factory for shared List instances. Allows for central setup @@ -35,12 +36,10 @@ */ public class ListFactoryBean extends AbstractFactoryBean> { - @Nullable - private List sourceList; + private @Nullable List sourceList; @SuppressWarnings("rawtypes") - @Nullable - private Class targetListClass; + private @Nullable Class targetListClass; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java index b02673c8b789..307fe53ba082 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MapFactoryBean.java @@ -18,10 +18,11 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -35,12 +36,10 @@ */ public class MapFactoryBean extends AbstractFactoryBean> { - @Nullable - private Map sourceMap; + private @Nullable Map sourceMap; @SuppressWarnings("rawtypes") - @Nullable - private Class targetMapClass; + private @Nullable Class targetMapClass; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java index eb5ae4c0b7af..be10715dd2a2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingBean.java @@ -18,13 +18,14 @@ import java.lang.reflect.InvocationTargetException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.support.ArgumentConvertingMethodInvoker; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -67,11 +68,9 @@ public class MethodInvokingBean extends ArgumentConvertingMethodInvoker implements BeanClassLoaderAware, BeanFactoryAware, InitializingBean { - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private ConfigurableBeanFactory beanFactory; + private @Nullable ConfigurableBeanFactory beanFactory; @Override @@ -117,8 +116,7 @@ public void afterPropertiesSet() throws Exception { * Perform the invocation and convert InvocationTargetException * into the underlying target exception. */ - @Nullable - protected Object invokeWithTargetException() throws Exception { + protected @Nullable Object invokeWithTargetException() throws Exception { try { return invoke(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java index ec89f904e4f4..6e6a4a8a6297 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/MethodInvokingFactoryBean.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.FactoryBeanNotInitializedException; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} which returns a value which is the result of a static or instance @@ -88,8 +89,7 @@ public class MethodInvokingFactoryBean extends MethodInvokingBean implements Fac private boolean initialized = false; /** Method call result in the singleton case. */ - @Nullable - private Object singletonObject; + private @Nullable Object singletonObject; /** @@ -116,8 +116,7 @@ public void afterPropertiesSet() throws Exception { * specified method on the fly. */ @Override - @Nullable - public Object getObject() throws Exception { + public @Nullable Object getObject() throws Exception { if (this.singleton) { if (!this.initialized) { throw new FactoryBeanNotInitializedException(); @@ -136,8 +135,7 @@ public Object getObject() throws Exception { * or {@code null} if not known in advance. */ @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { if (!isPrepared()) { // Not fully initialized yet -> return null to indicate "not known yet". return null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBean.java index e1f9208ad8c2..1474a3879ec1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ObjectFactoryCreatingFactoryBean.java @@ -18,10 +18,11 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.ObjectFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -97,8 +98,7 @@ */ public class ObjectFactoryCreatingFactoryBean extends AbstractFactoryBean> { - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; /** 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 6e37fc17fb46..5ff44380d898 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. @@ -16,12 +16,15 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + 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.lang.Nullable; +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 @@ -37,16 +40,16 @@ * *
  * <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: @@ -79,11 +82,12 @@ *

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 @@ -92,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; @@ -110,30 +119,27 @@ public abstract class PlaceholderConfigurerSupport extends PropertyResourceConfi protected String placeholderSuffix = DEFAULT_PLACEHOLDER_SUFFIX; /** Defaults to {@value #DEFAULT_VALUE_SEPARATOR}. */ - @Nullable - protected String valueSeparator = DEFAULT_VALUE_SEPARATOR; + protected @Nullable String valueSeparator = DEFAULT_VALUE_SEPARATOR; - /** Defaults to {@link #DEFAULT_ESCAPE_CHARACTER}. */ - @Nullable - protected Character escapeCharacter = DEFAULT_ESCAPE_CHARACTER; + /** + * The default is determined by {@link AbstractPropertyResolver#getDefaultEscapeCharacter()}. + */ + protected @Nullable Character escapeCharacter = AbstractPropertyResolver.getDefaultEscapeCharacter(); protected boolean trimValues = false; - @Nullable - protected String nullValue; + protected @Nullable String nullValue; protected boolean ignoreUnresolvablePlaceholders = false; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** * 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; @@ -141,31 +147,32 @@ public void setPlaceholderPrefix(String placeholderPrefix) { /** * 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; } /** @@ -228,7 +235,6 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - @SuppressWarnings("NullAway") protected void doProcessProperties(ConfigurableListableBeanFactory beanFactoryToProcess, StringValueResolver valueResolver) { 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 fc616b895ef9..f4a28b3e14f3 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. @@ -20,13 +20,14 @@ import java.util.prefs.BackingStoreException; import java.util.prefs.Preferences; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** - * 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. @@ -42,16 +43,15 @@ * @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 { - @Nullable - private String systemTreePath; + private @Nullable String systemTreePath; - @Nullable - private String userTreePath; + private @Nullable String userTreePath; private Preferences systemPrefs = Preferences.systemRoot(); @@ -120,8 +120,7 @@ protected String resolvePlaceholder(String placeholder, Properties props) { * @param preferences the Preferences to resolve against * @return the value for the placeholder, or {@code null} if none found */ - @Nullable - protected String resolvePlaceholder(@Nullable String path, String key, Preferences preferences) { + protected @Nullable String resolvePlaceholder(@Nullable String path, String key, Preferences preferences) { if (path != null) { // Do not create the node if it does not exist... try { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java index 47a857eb1f24..803d59324117 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertiesFactoryBean.java @@ -19,10 +19,11 @@ import java.io.IOException; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.support.PropertiesLoaderSupport; -import org.springframework.lang.Nullable; /** * Allows for making a properties file from a classpath location available @@ -48,8 +49,7 @@ public class PropertiesFactoryBean extends PropertiesLoaderSupport private boolean singleton = true; - @Nullable - private Properties singletonInstance; + private @Nullable Properties singletonInstance; /** @@ -75,8 +75,7 @@ public final void afterPropertiesSet() throws IOException { } @Override - @Nullable - public final Properties getObject() throws IOException { + public final @Nullable Properties getObject() throws IOException { if (this.singleton) { return this.singletonInstance; } 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 840a34e76234..0a26f2041965 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 @@ * * 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/PropertyPathFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java index b22e52be3d26..a07977819aec 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/PropertyPathFactoryBean.java @@ -18,6 +18,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeansException; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -87,23 +87,19 @@ public class PropertyPathFactoryBean implements FactoryBean, BeanNameAwa private static final Log logger = LogFactory.getLog(PropertyPathFactoryBean.class); - @Nullable - private BeanWrapper targetBeanWrapper; + private @Nullable BeanWrapper targetBeanWrapper; - @Nullable + @SuppressWarnings("NullAway.Init") private String targetBeanName; - @Nullable - private String propertyPath; + private @Nullable String propertyPath; - @Nullable - private Class resultType; + private @Nullable Class resultType; - @Nullable + @SuppressWarnings("NullAway.Init") private String beanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** @@ -162,7 +158,6 @@ public void setBeanName(String beanName) { @Override - @SuppressWarnings("NullAway") public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; @@ -202,8 +197,7 @@ else if (this.propertyPath == null) { @Override - @Nullable - public Object getObject() throws BeansException { + public @Nullable Object getObject() throws BeansException { BeanWrapper target = this.targetBeanWrapper; if (target != null) { if (logger.isWarnEnabled() && this.targetBeanName != null && @@ -225,8 +219,7 @@ public Object getObject() throws BeansException { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.resultType; } 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 51d9124850ca..81d5eb24df22 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. @@ -19,10 +19,11 @@ import java.util.Map; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.core.SpringProperties; import org.springframework.core.env.AbstractEnvironment; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PropertyPlaceholderHelper; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; @@ -51,11 +52,13 @@ * @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. */ @@ -153,8 +156,7 @@ public void setSearchSystemEnvironment(boolean searchSystemEnvironment) { * @see System#getProperty * @see #resolvePlaceholder(String, java.util.Properties) */ - @Nullable - protected String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) { + protected @Nullable String resolvePlaceholder(String placeholder, Properties props, int systemPropertiesMode) { String propVal = null; if (systemPropertiesMode == SYSTEM_PROPERTIES_MODE_OVERRIDE) { propVal = resolveSystemProperty(placeholder); @@ -181,8 +183,7 @@ protected String resolvePlaceholder(String placeholder, Properties props, int sy * @return the resolved value, of {@code null} if none * @see #setSystemPropertiesMode */ - @Nullable - protected String resolvePlaceholder(String placeholder, Properties props) { + protected @Nullable String resolvePlaceholder(String placeholder, Properties props) { return props.getProperty(placeholder); } @@ -195,8 +196,7 @@ protected String resolvePlaceholder(String placeholder, Properties props) { * @see System#getProperty(String) * @see System#getenv(String) */ - @Nullable - protected String resolveSystemProperty(String key) { + protected @Nullable String resolveSystemProperty(String key) { try { String value = System.getProperty(key); if (value == null && this.searchSystemEnvironment) { @@ -240,8 +240,7 @@ public PlaceholderResolvingStringValueResolver(Properties props) { } @Override - @Nullable - public String resolveStringValue(String strVal) throws BeansException { + public @Nullable String resolveStringValue(String strVal) throws BeansException { String resolved = this.helper.replacePlaceholders(strVal, this.resolver); if (trimValues) { resolved = resolved.trim(); @@ -260,8 +259,7 @@ private PropertyPlaceholderConfigurerResolver(Properties props) { } @Override - @Nullable - public String resolvePlaceholder(String placeholderName) { + public @Nullable String resolvePlaceholder(String placeholderName) { return PropertyPlaceholderConfigurer.this.resolvePlaceholder(placeholderName, this.props, systemPropertiesMode); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java index 96d7bf3a6035..9fcb489660b8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ProviderCreatingFactoryBean.java @@ -19,10 +19,10 @@ import java.io.Serializable; import jakarta.inject.Provider; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,8 +43,7 @@ */ public class ProviderCreatingFactoryBean extends AbstractFactoryBean> { - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java index f04a8d103efd..ad8be03a389b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanNameReference.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -33,8 +34,7 @@ public class RuntimeBeanNameReference implements BeanReference { private final String beanName; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -60,8 +60,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java index 361b786dd39a..f96518c7c332 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/RuntimeBeanReference.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -33,13 +34,11 @@ public class RuntimeBeanReference implements BeanReference { private final String beanName; - @Nullable - private final Class beanType; + private final @Nullable Class beanType; private final boolean toParent; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -103,8 +102,7 @@ public String getBeanName() { * Return the requested bean type if resolution by type is demanded. * @since 5.2 */ - @Nullable - public Class getBeanType() { + public @Nullable Class getBeanType() { return this.beanType; } @@ -124,8 +122,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java index 8073dda0d912..cb4cb0de1d7b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/Scope.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.config; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.ObjectFactory; -import org.springframework.lang.Nullable; /** * Strategy interface used by a {@link ConfigurableBeanFactory}, @@ -89,8 +90,7 @@ public interface Scope { * @throws IllegalStateException if the underlying scope is not currently active * @see #registerDestructionCallback */ - @Nullable - Object remove(String name); + @Nullable Object remove(String name); /** * Register a callback to be executed on destruction of the specified @@ -130,8 +130,7 @@ public interface Scope { * @return the corresponding object, or {@code null} if none found * @throws IllegalStateException if the underlying scope is not currently active */ - @Nullable - Object resolveContextualObject(String key); + @Nullable Object resolveContextualObject(String key); /** * Return the conversation ID for the current underlying scope, if any. @@ -148,7 +147,6 @@ public interface Scope { * conversation ID for the current scope * @throws IllegalStateException if the underlying scope is not currently active */ - @Nullable - String getConversationId(); + @Nullable String getConversationId(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java index 590280998d59..95ca1024e5eb 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/ServiceLocatorFactoryBean.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,8 @@ import java.lang.reflect.Proxy; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.FatalBeanException; @@ -30,7 +32,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -190,20 +191,15 @@ */ public class ServiceLocatorFactoryBean implements FactoryBean, BeanFactoryAware, InitializingBean { - @Nullable - private Class serviceLocatorInterface; + private @Nullable Class serviceLocatorInterface; - @Nullable - private Constructor serviceLocatorExceptionConstructor; + private @Nullable Constructor serviceLocatorExceptionConstructor; - @Nullable - private Properties serviceMappings; + private @Nullable Properties serviceMappings; - @Nullable - private ListableBeanFactory beanFactory; + private @Nullable ListableBeanFactory beanFactory; - @Nullable - private Object proxy; + private @Nullable Object proxy; /** @@ -315,7 +311,7 @@ protected Constructor determineServiceLocatorExceptionConstructor(Cla */ protected Exception createServiceLocatorException(Constructor exceptionConstructor, BeansException cause) { Class[] paramTypes = exceptionConstructor.getParameterTypes(); - Object[] args = new Object[paramTypes.length]; + @Nullable Object[] args = new Object[paramTypes.length]; for (int i = 0; i < paramTypes.length; i++) { if (String.class == paramTypes[i]) { args[i] = cause.getMessage(); @@ -329,14 +325,12 @@ else if (paramTypes[i].isInstance(cause)) { @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.proxy; } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.serviceLocatorInterface; } @@ -394,7 +388,7 @@ private Object invokeServiceLocatorMethod(Method method, Object[] args) throws E /** * Check whether a service id was passed in. */ - private String tryGetBeanName(@Nullable Object[] args) { + private String tryGetBeanName(Object @Nullable [] args) { String beanName = ""; if (args != null && args.length == 1 && args[0] != null) { beanName = args[0].toString(); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java index 8b30f8eb8f48..6b579da35327 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SetFactoryBean.java @@ -18,10 +18,11 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -35,12 +36,10 @@ */ public class SetFactoryBean extends AbstractFactoryBean> { - @Nullable - private Set sourceSet; + private @Nullable Set sourceSet; @SuppressWarnings("rawtypes") - @Nullable - private Class targetSetClass; + private @Nullable Class targetSetClass; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java index b1f9f876b425..0da3086b9ee4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SingletonBeanRegistry.java @@ -18,7 +18,7 @@ import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that defines a registry for shared bean instances. @@ -83,8 +83,7 @@ public interface SingletonBeanRegistry { * @return the registered singleton object, or {@code null} if none found * @see ConfigurableListableBeanFactory#getBeanDefinition */ - @Nullable - Object getSingleton(String beanName); + @Nullable Object getSingleton(String beanName); /** * Check if this registry contains a singleton instance with the given name. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java index 86455b173c11..79aae284a20f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/SmartInstantiationAwareBeanPostProcessor.java @@ -18,8 +18,9 @@ import java.lang.reflect.Constructor; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; /** * Extension of the {@link InstantiationAwareBeanPostProcessor} interface, @@ -46,8 +47,7 @@ public interface SmartInstantiationAwareBeanPostProcessor extends InstantiationA * @return the type of the bean, or {@code null} if not predictable * @throws org.springframework.beans.BeansException in case of errors */ - @Nullable - default Class predictBeanType(Class beanClass, String beanName) throws BeansException { + default @Nullable Class predictBeanType(Class beanClass, String beanName) throws BeansException { return null; } @@ -75,8 +75,7 @@ default Class determineBeanType(Class beanClass, String beanName) throws B * @return the candidate constructors, or {@code null} if none specified * @throws org.springframework.beans.BeansException in case of errors */ - @Nullable - default Constructor[] determineCandidateConstructors(Class beanClass, String beanName) + default Constructor @Nullable [] determineCandidateConstructors(Class beanClass, String beanName) throws BeansException { return null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java index c4d9c5c8e540..b807dd4cb0a6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/TypedStringValue.java @@ -18,8 +18,9 @@ import java.util.Comparator; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -39,17 +40,13 @@ */ public class TypedStringValue implements BeanMetadataElement, Comparable { - @Nullable - private String value; + private @Nullable String value; - @Nullable - private volatile Object targetType; + private volatile @Nullable Object targetType; - @Nullable - private Object source; + private @Nullable Object source; - @Nullable - private String specifiedTypeName; + private @Nullable String specifiedTypeName; private volatile boolean dynamic; @@ -97,8 +94,7 @@ public void setValue(@Nullable String value) { /** * Return the String value. */ - @Nullable - public String getValue() { + public @Nullable String getValue() { return this.value; } @@ -133,8 +129,7 @@ public void setTargetTypeName(@Nullable String targetTypeName) { /** * Return the type to convert to. */ - @Nullable - public String getTargetTypeName() { + public @Nullable String getTargetTypeName() { Object targetTypeValue = this.targetType; if (targetTypeValue instanceof Class clazz) { return clazz.getName(); @@ -159,8 +154,7 @@ public boolean hasTargetType() { * @return the resolved type to convert to * @throws ClassNotFoundException if the type cannot be resolved */ - @Nullable - public Class resolveTargetType(@Nullable ClassLoader classLoader) throws ClassNotFoundException { + public @Nullable Class resolveTargetType(@Nullable ClassLoader classLoader) throws ClassNotFoundException { String typeName = getTargetTypeName(); if (typeName == null) { return null; @@ -180,8 +174,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -195,8 +188,7 @@ public void setSpecifiedTypeName(@Nullable String specifiedTypeName) { /** * Return the type name as actually specified for this particular value, if any. */ - @Nullable - public String getSpecifiedTypeName() { + public @Nullable String getSpecifiedTypeName() { return this.specifiedTypeName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java index ea482766d782..ac3f26e43320 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlMapFactoryBean.java @@ -19,9 +19,10 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * Factory for a {@code Map} that reads from a YAML source, preserving the @@ -74,8 +75,7 @@ public class YamlMapFactoryBean extends YamlProcessor implements FactoryBean map; + private @Nullable Map map; /** @@ -99,8 +99,7 @@ public void afterPropertiesSet() { } @Override - @Nullable - public Map getObject() { + public @Nullable Map getObject() { return (this.map != null ? this.map : createMap()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java index 472188fd389f..e0d332027f0f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlProcessor.java @@ -30,6 +30,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.yaml.snakeyaml.DumperOptions; import org.yaml.snakeyaml.LoaderOptions; import org.yaml.snakeyaml.Yaml; @@ -42,7 +43,6 @@ import org.springframework.core.CollectionFactory; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java index 5c09abda686f..670294147aab 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/YamlPropertiesFactoryBean.java @@ -18,10 +18,11 @@ import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.CollectionFactory; -import org.springframework.lang.Nullable; /** * Factory for {@link java.util.Properties} that reads from a YAML source, @@ -85,8 +86,7 @@ public class YamlPropertiesFactoryBean extends YamlProcessor implements FactoryB private boolean singleton = true; - @Nullable - private Properties properties; + private @Nullable Properties properties; /** @@ -110,8 +110,7 @@ public void afterPropertiesSet() { } @Override - @Nullable - public Properties getObject() { + public @Nullable Properties getObject() { return (this.properties != null ? this.properties : createProperties()); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java index 280e916ab186..5ba69e387644 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/config/package-info.java @@ -1,9 +1,7 @@ /** * SPI interfaces and configuration-related convenience classes for bean factories. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java index 0d9a67cd04b6..d28910333630 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionReader.java @@ -33,6 +33,7 @@ import groovy.lang.MetaClass; import org.codehaus.groovy.runtime.DefaultGroovyMethods; import org.codehaus.groovy.runtime.InvokerHelper; +import org.jspecify.annotations.Nullable; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.BeanDefinitionStoreException; @@ -53,7 +54,6 @@ import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -151,11 +151,9 @@ public class GroovyBeanDefinitionReader extends AbstractBeanDefinitionReader imp private MetaClass metaClass = GroovySystem.getMetaClassRegistry().getMetaClass(getClass()); - @Nullable - private Binding binding; + private @Nullable Binding binding; - @Nullable - private GroovyBeanDefinitionWrapper currentBeanDefinition; + private @Nullable GroovyBeanDefinitionWrapper currentBeanDefinition; /** @@ -207,8 +205,7 @@ public void setBinding(Binding binding) { /** * Return a specified binding for Groovy variables, if any. */ - @Nullable - public Binding getBinding() { + public @Nullable Binding getBinding() { return this.binding; } @@ -251,8 +248,7 @@ public int loadBeanDefinitions(EncodedResource encodedResource) throws BeanDefin @SuppressWarnings("serial") Closure beans = new Closure<>(this) { @Override - @Nullable - public Object call(Object... args) { + public @Nullable Object call(Object... args) { invokeBeanDefiningClosure((Closure) args[0]); return null; } @@ -658,8 +654,7 @@ else if (value instanceof Closure callable) { * */ @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { Binding binding = getBinding(); if (binding != null && binding.hasVariable(name)) { return binding.getVariable(name); @@ -701,7 +696,7 @@ else if (this.currentBeanDefinition != null) { } } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private GroovyDynamicElementReader createDynamicElementReader(String namespace) { XmlReaderContext readerContext = this.groovyDslXmlBeanDefinitionReader.createReaderContext( new DescriptiveResource("Groovy")); @@ -733,8 +728,7 @@ private static class DeferredProperty { private final String name; - @Nullable - public Object value; + public @Nullable Object value; public DeferredProperty(GroovyBeanDefinitionWrapper beanDefinition, String name, @Nullable Object value) { this.beanDefinition = beanDefinition; @@ -769,8 +763,7 @@ public MetaClass getMetaClass() { } @Override - @Nullable - public Object getProperty(String property) { + public @Nullable Object getProperty(String property) { if (property.equals("beanName")) { return getBeanName(); } @@ -809,8 +802,7 @@ private class GroovyPropertyValue extends GroovyObjectSupport { private final String propertyName; - @Nullable - private final Object propertyValue; + private final @Nullable Object propertyValue; public GroovyPropertyValue(String propertyName, @Nullable Object propertyValue) { this.propertyName = propertyName; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java index 895646d9da0b..b0f71df9bbac 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyBeanDefinitionWrapper.java @@ -21,6 +21,7 @@ import java.util.Set; import groovy.lang.GroovyObjectSupport; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; @@ -30,7 +31,6 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -57,23 +57,17 @@ class GroovyBeanDefinitionWrapper extends GroovyObjectSupport { FACTORY_BEAN, FACTORY_METHOD, INIT_METHOD, DESTROY_METHOD, SINGLETON); - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private final Class clazz; + private final @Nullable Class clazz; - @Nullable - private final Collection constructorArgs; + private final @Nullable Collection constructorArgs; - @Nullable - private AbstractBeanDefinition definition; + private @Nullable AbstractBeanDefinition definition; - @Nullable - private BeanWrapper definitionWrapper; + private @Nullable BeanWrapper definitionWrapper; - @Nullable - private String parentName; + private @Nullable String parentName; GroovyBeanDefinitionWrapper(String beanName) { @@ -91,8 +85,7 @@ class GroovyBeanDefinitionWrapper extends GroovyObjectSupport { } - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } @@ -159,8 +152,7 @@ GroovyBeanDefinitionWrapper addProperty(String propertyName, @Nullable Object pr @Override - @Nullable - public Object getProperty(String property) { + public @Nullable Object getProperty(String property) { Assert.state(this.definitionWrapper != null, "BeanDefinition wrapper not initialized"); if (this.definitionWrapper.isReadableProperty(property)) { return this.definitionWrapper.getPropertyValue(property); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java index b8b9efd52031..31e9d9de5edf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/GroovyDynamicElementReader.java @@ -25,13 +25,13 @@ import groovy.lang.GroovyObjectSupport; import groovy.lang.Writable; import groovy.xml.StreamingMarkupBuilder; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; -import org.springframework.lang.Nullable; /** * Used by GroovyBeanDefinitionReader to read a Spring XML namespace expression @@ -69,8 +69,7 @@ public GroovyDynamicElementReader(String namespace, Map namespac @Override - @Nullable - public Object invokeMethod(String name, Object obj) { + public @Nullable Object invokeMethod(String name, Object obj) { Object[] args = (Object[]) obj; if (name.equals("doCall")) { @SuppressWarnings("unchecked") diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/package-info.java index 9201a5278280..a48700cf4625 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/groovy/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/groovy/package-info.java @@ -1,9 +1,7 @@ /** * Support package for Groovy-based bean definitions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.groovy; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java index a29b453f3ee4..48c6b5c60b04 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/package-info.java @@ -9,9 +9,7 @@ * Expert One-On-One J2EE Design and Development * by Rod Johnson (Wrox, 2002). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AliasDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AliasDefinition.java index 27bed9e3c90c..875b6a81d8ad 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AliasDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/AliasDefinition.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -33,8 +34,7 @@ public class AliasDefinition implements BeanMetadataElement { private final String alias; - @Nullable - private final Object source; + private final @Nullable Object source; /** @@ -76,8 +76,7 @@ public final String getAlias() { } @Override - @Nullable - public final Object getSource() { + public final @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java index ad8e72ed48f3..4dd2c97ba41f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/BeanComponentDefinition.java @@ -19,12 +19,13 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyValue; import org.springframework.beans.PropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanReference; -import org.springframework.lang.Nullable; /** * ComponentDefinition based on a standard BeanDefinition, exposing the given bean @@ -56,7 +57,7 @@ public BeanComponentDefinition(BeanDefinition beanDefinition, String beanName) { * @param beanName the name of the bean * @param aliases alias names for the bean, or {@code null} if none */ - public BeanComponentDefinition(BeanDefinition beanDefinition, String beanName, @Nullable String[] aliases) { + public BeanComponentDefinition(BeanDefinition beanDefinition, String beanName, String @Nullable [] aliases) { this(new BeanDefinitionHolder(beanDefinition, beanName, aliases)); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/CompositeComponentDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/CompositeComponentDefinition.java index efd846cedae7..e7658cd8199a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/CompositeComponentDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/CompositeComponentDefinition.java @@ -19,7 +19,8 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -35,8 +36,7 @@ public class CompositeComponentDefinition extends AbstractComponentDefinition { private final String name; - @Nullable - private final Object source; + private final @Nullable Object source; private final List nestedComponents = new ArrayList<>(); @@ -59,8 +59,7 @@ public String getName() { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/FailFastProblemReporter.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/FailFastProblemReporter.java index 3dc71bb4b35d..1de71d5c927f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/FailFastProblemReporter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/FailFastProblemReporter.java @@ -18,8 +18,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple {@link ProblemReporter} implementation that exhibits fail-fast diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ImportDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ImportDefinition.java index 374025022155..861fc64379d2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ImportDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ImportDefinition.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -32,11 +33,9 @@ public class ImportDefinition implements BeanMetadataElement { private final String importedResource; - @Nullable - private final Resource[] actualResources; + private final Resource @Nullable [] actualResources; - @Nullable - private final Object source; + private final @Nullable Object source; /** @@ -61,7 +60,7 @@ public ImportDefinition(String importedResource, @Nullable Object source) { * @param importedResource the location of the imported resource * @param source the source object (may be {@code null}) */ - public ImportDefinition(String importedResource, @Nullable Resource[] actualResources, @Nullable Object source) { + public ImportDefinition(String importedResource, Resource @Nullable [] actualResources, @Nullable Object source) { Assert.notNull(importedResource, "Imported resource must not be null"); this.importedResource = importedResource; this.actualResources = actualResources; @@ -76,14 +75,12 @@ public final String getImportedResource() { return this.importedResource; } - @Nullable - public final Resource[] getActualResources() { + public final Resource @Nullable [] getActualResources() { return this.actualResources; } @Override - @Nullable - public final Object getSource() { + public final @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Location.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Location.java index b06c524ce041..50b77dc5345f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Location.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Location.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ public class Location { private final Resource resource; - @Nullable - private final Object source; + private final @Nullable Object source; /** @@ -75,8 +75,7 @@ public Resource getResource() { *

See the {@link Location class level javadoc for this class} for examples * of what the actual type of the returned object may be. */ - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/NullSourceExtractor.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/NullSourceExtractor.java index 1205b3fa8e6a..165667d9e941 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/NullSourceExtractor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/NullSourceExtractor.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Simple implementation of {@link SourceExtractor} that returns {@code null} @@ -35,8 +36,7 @@ public class NullSourceExtractor implements SourceExtractor { * This implementation simply returns {@code null} for any input. */ @Override - @Nullable - public Object extractSource(Object sourceCandidate, @Nullable Resource definitionResource) { + public @Nullable Object extractSource(Object sourceCandidate, @Nullable Resource definitionResource) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java index afbb13957125..c1b1afb0fefe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ParseState.java @@ -18,7 +18,7 @@ import java.util.ArrayDeque; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple {@link ArrayDeque}-based structure for tracking the logical position during @@ -74,8 +74,7 @@ public void pop() { * Return the {@link Entry} currently at the top of the {@link ArrayDeque} or * {@code null} if the {@link ArrayDeque} is empty. */ - @Nullable - public Entry peek() { + public @Nullable Entry peek() { return this.state.peek(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractor.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractor.java index 1365c9932b6c..1d4525459250 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/PassThroughSourceExtractor.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Simple {@link SourceExtractor} implementation that just passes diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Problem.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Problem.java index 86d58000d117..e724c5e85e2e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Problem.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/Problem.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.parsing; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -36,11 +37,9 @@ public class Problem { private final Location location; - @Nullable - private final ParseState parseState; + private final @Nullable ParseState parseState; - @Nullable - private final Throwable rootCause; + private final @Nullable Throwable rootCause; /** @@ -105,16 +104,14 @@ public String getResourceDescription() { /** * Get the {@link ParseState} at the time of the error (may be {@code null}). */ - @Nullable - public ParseState getParseState() { + public @Nullable ParseState getParseState() { return this.parseState; } /** * Get the underlying exception that caused the error (may be {@code null}). */ - @Nullable - public Throwable getRootCause() { + public @Nullable Throwable getRootCause() { return this.rootCause; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java index 2b95aa8f6c3b..7879ba648ec4 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/ReaderContext.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Context that gets passed along a bean definition reading process, @@ -203,8 +204,7 @@ public SourceExtractor getSourceExtractor() { * @see #getSourceExtractor() * @see SourceExtractor#extractSource */ - @Nullable - public Object extractSource(Object sourceCandidate) { + public @Nullable Object extractSource(Object sourceCandidate) { return this.sourceExtractor.extractSource(sourceCandidate, this.resource); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/SourceExtractor.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/SourceExtractor.java index 8809cd2473f3..960b0b3ae744 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/SourceExtractor.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/SourceExtractor.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.parsing; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Simple strategy allowing tools to control how source metadata is attached @@ -45,7 +46,6 @@ public interface SourceExtractor { * (may be {@code null}) * @return the source metadata object to store (may be {@code null}) */ - @Nullable - Object extractSource(Object sourceCandidate, @Nullable Resource definingResource); + @Nullable Object extractSource(Object sourceCandidate, @Nullable Resource definingResource); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/package-info.java index 0f57ef135159..dcb31b3e7359 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/parsing/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/parsing/package-info.java @@ -1,9 +1,7 @@ /** * Support infrastructure for bean definition parsing. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.parsing; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java index 3ee514663c68..27f3342d7ebd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/AbstractServiceLoaderBasedFactoryBean.java @@ -18,9 +18,10 @@ import java.util.ServiceLoader; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.config.AbstractFactoryBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -35,11 +36,9 @@ public abstract class AbstractServiceLoaderBasedFactoryBean extends AbstractFactoryBean implements BeanClassLoaderAware { - @Nullable - private Class serviceType; + private @Nullable Class serviceType; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** @@ -52,8 +51,7 @@ public void setServiceType(@Nullable Class serviceType) { /** * Return the desired service type. */ - @Nullable - public Class getServiceType() { + public @Nullable Class getServiceType() { return this.serviceType; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java index 535a53716e06..1698abcdc210 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/ServiceFactoryBean.java @@ -19,8 +19,9 @@ import java.util.Iterator; import java.util.ServiceLoader; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; /** * {@link org.springframework.beans.factory.FactoryBean} that exposes the @@ -44,8 +45,7 @@ protected Object getObjectToExpose(ServiceLoader serviceLoader) { } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return getServiceType(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java index b6a97c2c7ef6..5c6a93356398 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/serviceloader/package-info.java @@ -1,9 +1,7 @@ /** * Support package for the Java {@link java.util.ServiceLoader} facility. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.serviceloader; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 131c0313cfd6..bdd1e9f8361b 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 @@ -34,6 +34,7 @@ import java.util.function.Supplier; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; @@ -73,7 +74,6 @@ import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.PriorityOrdered; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -125,8 +125,7 @@ public abstract class AbstractAutowireCapableBeanFactory extends AbstractBeanFac private InstantiationStrategy instantiationStrategy; /** Resolver strategy for method parameter names. */ - @Nullable - private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + private @Nullable ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); /** Whether to automatically try to resolve circular references between beans. */ private boolean allowCircularReferences = true; @@ -144,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<>(); @@ -216,8 +217,7 @@ public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer paramet * Return the ParameterNameDiscoverer to use for resolving method parameter * names if needed. */ - @Nullable - public ParameterNameDiscoverer getParameterNameDiscoverer() { + public @Nullable ParameterNameDiscoverer getParameterNameDiscoverer() { return this.parameterNameDiscoverer; } @@ -285,11 +285,15 @@ public void ignoreDependencyType(Class type) { /** * 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) { @@ -359,7 +363,7 @@ public Object configureBean(Object existingBean, String beanName) throws BeansEx // 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. @@ -467,8 +471,7 @@ public Object resolveBeanByName(String name, DependencyDescriptor descriptor) { } @Override - @Nullable - public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException { + public @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) throws BeansException { return resolveDependency(descriptor, requestingBeanName, null, null); } @@ -483,7 +486,7 @@ public Object resolveDependency(DependencyDescriptor descriptor, @Nullable Strin * @see #doCreateBean */ @Override - protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] args) throws BeanCreationException { if (logger.isTraceEnabled()) { @@ -551,7 +554,7 @@ protected Object createBean(String beanName, RootBeanDefinition mbd, @Nullable O * @see #instantiateUsingFactoryMethod * @see #autowireConstructor */ - protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] args) throws BeanCreationException { // Instantiate the bean. @@ -649,8 +652,7 @@ else if (!this.allowRawInjectionDespiteWrapping && hasDependentBean(beanName)) { } @Override - @Nullable - protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { + protected @Nullable Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { Class targetType = determineTargetType(beanName, mbd, typesToMatch); // Apply SmartInstantiationAwareBeanPostProcessors to predict the // eventual type after a before-instantiation shortcut. @@ -675,8 +677,7 @@ protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Clas * (also signals that the returned {@code Class} will never be exposed to application code) * @return the type for the bean if determinable, or {@code null} otherwise */ - @Nullable - protected Class determineTargetType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { + protected @Nullable Class determineTargetType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { Class targetType = mbd.getTargetType(); if (targetType == null) { if (mbd.getFactoryMethodName() != null) { @@ -709,8 +710,7 @@ protected Class determineTargetType(String beanName, RootBeanDefinition mbd, * @return the type for the bean if determinable, or {@code null} otherwise * @see #createBean */ - @Nullable - protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { + protected @Nullable Class getTypeForFactoryMethod(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { ResolvableType cachedReturnType = mbd.factoryMethodReturnType; if (cachedReturnType != null) { return cachedReturnType.resolve(); @@ -759,7 +759,7 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m // Fully resolve parameter names and argument values. ConstructorArgumentValues cav = mbd.getConstructorArgumentValues(); Class[] paramTypes = candidate.getParameterTypes(); - String[] paramNames = null; + @Nullable String[] paramNames = null; if (cav.containsNamedArgument()) { ParameterNameDiscoverer pnd = getParameterNameDiscoverer(); if (pnd != null) { @@ -767,7 +767,7 @@ protected Class getTypeForFactoryMethod(String beanName, RootBeanDefinition m } } Set usedValueHolders = CollectionUtils.newHashSet(paramTypes.length); - Object[] args = new Object[paramTypes.length]; + @Nullable Object[] args = new Object[paramTypes.length]; for (int i = 0; i < args.length; i++) { ConstructorArgumentValues.ValueHolder valueHolder = cav.getArgumentValue( i, paramTypes[i], (paramNames != null ? paramNames[i] : null), usedValueHolders); @@ -989,11 +989,18 @@ protected Object getEarlyBeanReference(String beanName, RootBeanDefinition mbd, * @return the FactoryBean instance, or {@code null} to indicate * that we couldn't obtain a shortcut FactoryBean instance */ - @Nullable - private FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { - boolean locked = this.singletonLock.tryLock(); - if (!locked) { - return null; + private @Nullable FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { + 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 { @@ -1059,8 +1066,7 @@ private FactoryBean getSingletonFactoryBeanForTypeCheck(String beanName, Root * @return the FactoryBean instance, or {@code null} to indicate * that we couldn't obtain a shortcut FactoryBean instance */ - @Nullable - private FactoryBean getNonSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { + private @Nullable FactoryBean getNonSingletonFactoryBeanForTypeCheck(String beanName, RootBeanDefinition mbd) { if (isPrototypeCurrentlyInCreation(beanName)) { return null; } @@ -1118,8 +1124,7 @@ protected void applyMergedBeanDefinitionPostProcessors(RootBeanDefinition mbd, C * @return the shortcut-determined bean instance, or {@code null} if none */ @SuppressWarnings("deprecation") - @Nullable - protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { + protected @Nullable Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) { Object bean = null; if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) { // Make sure bean class is actually resolved at this point. @@ -1148,8 +1153,7 @@ protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition * @return the bean object to use instead of a default instance of the target bean, or {@code null} * @see InstantiationAwareBeanPostProcessor#postProcessBeforeInstantiation */ - @Nullable - protected Object applyBeanPostProcessorsBeforeInstantiation(Class beanClass, String beanName) { + protected @Nullable Object applyBeanPostProcessorsBeforeInstantiation(Class beanClass, String beanName) { for (InstantiationAwareBeanPostProcessor bp : getBeanPostProcessorCache().instantiationAware) { Object result = bp.postProcessBeforeInstantiation(beanClass, beanName); if (result != null) { @@ -1171,7 +1175,7 @@ protected Object applyBeanPostProcessorsBeforeInstantiation(Class beanClass, * @see #autowireConstructor * @see #instantiateBean */ - protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) { + protected BeanWrapper createBeanInstance(String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] args) { // Make sure bean class is actually resolved at this point. Class beanClass = resolveBeanClass(mbd, beanName); @@ -1273,8 +1277,7 @@ private BeanWrapper obtainFromSupplier(Supplier supplier, String beanName, Ro * @return the bean instance (possibly {@code null}) * @since 6.0.7 */ - @Nullable - protected Object obtainInstanceFromSupplier(Supplier supplier, String beanName, RootBeanDefinition mbd) + protected @Nullable Object obtainInstanceFromSupplier(Supplier supplier, String beanName, RootBeanDefinition mbd) throws Exception { if (supplier instanceof ThrowingSupplier throwingSupplier) { @@ -1311,8 +1314,7 @@ protected Object getObjectForBeanInstance( * @throws org.springframework.beans.BeansException in case of errors * @see org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor#determineCandidateConstructors */ - @Nullable - protected Constructor[] determineConstructorsFromBeanPostProcessors(@Nullable Class beanClass, String beanName) + protected Constructor @Nullable [] determineConstructorsFromBeanPostProcessors(@Nullable Class beanClass, String beanName) throws BeansException { if (beanClass != null && hasInstantiationAwareBeanPostProcessors()) { @@ -1356,7 +1358,7 @@ protected BeanWrapper instantiateBean(String beanName, RootBeanDefinition mbd) { * @see #getBean(String, Object[]) */ protected BeanWrapper instantiateUsingFactoryMethod( - String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) { + String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] explicitArgs) { return new ConstructorResolver(this).instantiateUsingFactoryMethod(beanName, mbd, explicitArgs); } @@ -1376,7 +1378,7 @@ protected BeanWrapper instantiateUsingFactoryMethod( * @return a BeanWrapper for the new instance */ protected BeanWrapper autowireConstructor( - String beanName, RootBeanDefinition mbd, @Nullable Constructor[] ctors, @Nullable Object[] explicitArgs) { + String beanName, RootBeanDefinition mbd, Constructor @Nullable [] ctors, @Nullable Object @Nullable [] explicitArgs) { return new ConstructorResolver(this).autowireConstructor(beanName, mbd, ctors, explicitArgs); } @@ -1763,8 +1765,7 @@ private boolean isConvertibleProperty(String propertyName, BeanWrapper bw) { /** * Convert the given value for the specified target property. */ - @Nullable - private Object convertForProperty( + private @Nullable Object convertForProperty( @Nullable Object value, String propertyName, BeanWrapper bw, TypeConverter converter) { if (converter instanceof BeanWrapperImpl beanWrapper) { @@ -1978,8 +1979,7 @@ public CreateFromClassBeanDefinition(CreateFromClassBeanDefinition original) { } @Override - @Nullable - public Constructor[] getPreferredConstructors() { + public Constructor @Nullable [] getPreferredConstructors() { Constructor[] fromAttribute = super.getPreferredConstructors(); if (fromAttribute != null) { return fromAttribute; @@ -2006,8 +2006,7 @@ public AutowireByTypeDependencyDescriptor(MethodParameter methodParameter, boole } @Override - @Nullable - public String getDependencyName() { + public @Nullable String getDependencyName() { return null; } } 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 ab66ca7c8f39..c584eb56c22a 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. @@ -24,6 +24,8 @@ import java.util.Set; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataAttributeAccessor; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; @@ -32,7 +34,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -94,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; /** @@ -165,25 +166,21 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess public static final String INFER_METHOD = "(inferred)"; - @Nullable - private volatile Object beanClass; + private volatile @Nullable Object beanClass; - @Nullable - private String scope = SCOPE_DEFAULT; + private @Nullable String scope = SCOPE_DEFAULT; private boolean abstractFlag = false; private boolean backgroundInit = false; - @Nullable - private Boolean lazyInit; + private @Nullable Boolean lazyInit; private int autowireMode = AUTOWIRE_NO; private int dependencyCheck = DEPENDENCY_CHECK_NONE; - @Nullable - private String[] dependsOn; + private String @Nullable [] dependsOn; private boolean autowireCandidate = true; @@ -195,32 +192,25 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private final Map qualifiers = new LinkedHashMap<>(); - @Nullable - private Supplier instanceSupplier; + private @Nullable Supplier instanceSupplier; private boolean nonPublicAccessAllowed = true; private boolean lenientConstructorResolution = true; - @Nullable - private String factoryBeanName; + private @Nullable String factoryBeanName; - @Nullable - private String factoryMethodName; + private @Nullable String factoryMethodName; - @Nullable - private ConstructorArgumentValues constructorArgumentValues; + private @Nullable ConstructorArgumentValues constructorArgumentValues; - @Nullable - private MutablePropertyValues propertyValues; + private @Nullable MutablePropertyValues propertyValues; private MethodOverrides methodOverrides = new MethodOverrides(); - @Nullable - private String[] initMethodNames; + private String @Nullable [] initMethodNames; - @Nullable - private String[] destroyMethodNames; + private String @Nullable [] destroyMethodNames; private boolean enforceInitMethod = true; @@ -230,11 +220,9 @@ public abstract class AbstractBeanDefinition extends BeanMetadataAttributeAccess private int role = BeanDefinition.ROLE_APPLICATION; - @Nullable - private String description; + private @Nullable String description; - @Nullable - private Resource resource; + private @Nullable Resource resource; /** @@ -429,8 +417,7 @@ public void setBeanClassName(@Nullable String beanClassName) { * @see #getBeanClass() */ @Override - @Nullable - public String getBeanClassName() { + public @Nullable String getBeanClassName() { Object beanClassObject = this.beanClass; // defensive access to volatile beanClass field return (beanClassObject instanceof Class clazz ? clazz.getName() : (String) beanClassObject); } @@ -494,8 +481,7 @@ public boolean hasBeanClass() { * @return the resolved bean class * @throws ClassNotFoundException if the class name could be resolved */ - @Nullable - public Class resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException { + public @Nullable Class resolveBeanClass(@Nullable ClassLoader classLoader) throws ClassNotFoundException { String className = getBeanClassName(); if (className == null) { return null; @@ -534,8 +520,7 @@ public void setScope(@Nullable String scope) { *

The default is {@link #SCOPE_DEFAULT}. */ @Override - @Nullable - public String getScope() { + public @Nullable String getScope() { return this.scope; } @@ -631,8 +616,7 @@ public boolean isLazyInit() { * @return the lazy-init flag if explicitly set, or {@code null} otherwise * @since 5.2 */ - @Nullable - public Boolean getLazyInit() { + public @Nullable Boolean getLazyInit() { return this.lazyInit; } @@ -710,7 +694,7 @@ public int getDependencyCheck() { *

The default is no beans to explicitly depend on. */ @Override - public void setDependsOn(@Nullable String... dependsOn) { + public void setDependsOn(String @Nullable ... dependsOn) { this.dependsOn = dependsOn; } @@ -719,8 +703,7 @@ public void setDependsOn(@Nullable String... dependsOn) { *

The default is no beans to explicitly depend on. */ @Override - @Nullable - public String[] getDependsOn() { + public String @Nullable [] getDependsOn() { return this.dependsOn; } @@ -824,8 +807,7 @@ public boolean hasQualifier(String typeName) { /** * Return the qualifier mapped to the provided type name. */ - @Nullable - public AutowireCandidateQualifier getQualifier(String typeName) { + public @Nullable AutowireCandidateQualifier getQualifier(String typeName) { return this.qualifiers.get(typeName); } @@ -864,8 +846,7 @@ public void setInstanceSupplier(@Nullable Supplier instanceSupplier) { * Return a callback for creating an instance of the bean, if any. * @since 5.0 */ - @Nullable - public Supplier getInstanceSupplier() { + public @Nullable Supplier getInstanceSupplier() { return this.instanceSupplier; } @@ -922,8 +903,7 @@ public void setFactoryBeanName(@Nullable String factoryBeanName) { * @see #getBeanClass() */ @Override - @Nullable - public String getFactoryBeanName() { + public @Nullable String getFactoryBeanName() { return this.factoryBeanName; } @@ -943,8 +923,7 @@ public void setFactoryMethodName(@Nullable String factoryMethodName) { * @see RootBeanDefinition#getResolvedFactoryMethod() */ @Override - @Nullable - public String getFactoryMethodName() { + public @Nullable String getFactoryMethodName() { return this.factoryMethodName; } @@ -1038,7 +1017,7 @@ public boolean hasMethodOverrides() { * @since 6.0 * @see #setInitMethodName */ - public void setInitMethodNames(@Nullable String... initMethodNames) { + public void setInitMethodNames(String @Nullable ... initMethodNames) { this.initMethodNames = initMethodNames; } @@ -1046,8 +1025,7 @@ public void setInitMethodNames(@Nullable String... initMethodNames) { * Return the names of the initializer methods. * @since 6.0 */ - @Nullable - public String[] getInitMethodNames() { + public String @Nullable [] getInitMethodNames() { return this.initMethodNames; } @@ -1066,8 +1044,7 @@ public void setInitMethodName(@Nullable String initMethodName) { *

Use the first one in case of multiple methods. */ @Override - @Nullable - public String getInitMethodName() { + public @Nullable String getInitMethodName() { return (!ObjectUtils.isEmpty(this.initMethodNames) ? this.initMethodNames[0] : null); } @@ -1098,7 +1075,7 @@ public boolean isEnforceInitMethod() { * @since 6.0 * @see #setDestroyMethodName */ - public void setDestroyMethodNames(@Nullable String... destroyMethodNames) { + public void setDestroyMethodNames(String @Nullable ... destroyMethodNames) { this.destroyMethodNames = destroyMethodNames; } @@ -1106,8 +1083,7 @@ public void setDestroyMethodNames(@Nullable String... destroyMethodNames) { * Return the names of the destroy methods. * @since 6.0 */ - @Nullable - public String[] getDestroyMethodNames() { + public String @Nullable [] getDestroyMethodNames() { return this.destroyMethodNames; } @@ -1126,8 +1102,7 @@ public void setDestroyMethodName(@Nullable String destroyMethodName) { *

Use the first one in case of multiple methods. */ @Override - @Nullable - public String getDestroyMethodName() { + public @Nullable String getDestroyMethodName() { return (!ObjectUtils.isEmpty(this.destroyMethodNames) ? this.destroyMethodNames[0] : null); } @@ -1201,8 +1176,7 @@ public void setDescription(@Nullable String description) { *

The default is no description. */ @Override - @Nullable - public String getDescription() { + public @Nullable String getDescription() { return this.description; } @@ -1217,8 +1191,7 @@ public void setResource(@Nullable Resource resource) { /** * Return the resource that this bean definition came from. */ - @Nullable - public Resource getResource() { + public @Nullable Resource getResource() { return this.resource; } @@ -1235,8 +1208,7 @@ public void setResourceDescription(@Nullable String resourceDescription) { * @see #setResourceDescription */ @Override - @Nullable - public String getResourceDescription() { + public @Nullable String getResourceDescription() { return (this.resource != null ? this.resource.getDescription() : null); } @@ -1252,8 +1224,7 @@ public void setOriginatingBeanDefinition(BeanDefinition originatingBd) { * @see #setOriginatingBeanDefinition */ @Override - @Nullable - public BeanDefinition getOriginatingBeanDefinition() { + public @Nullable BeanDefinition getOriginatingBeanDefinition() { return (this.resource instanceof BeanDefinitionResource bdr ? bdr.getBeanDefinition() : null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java index 40a4503551ef..97635a095802 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AbstractBeanDefinitionReader.java @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.core.env.Environment; @@ -31,7 +32,6 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -53,11 +53,9 @@ public abstract class AbstractBeanDefinitionReader implements BeanDefinitionRead private final BeanDefinitionRegistry registry; - @Nullable - private ResourceLoader resourceLoader; + private @Nullable ResourceLoader resourceLoader; - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; private Environment environment; @@ -124,8 +122,7 @@ public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { } @Override - @Nullable - public ResourceLoader getResourceLoader() { + public @Nullable ResourceLoader getResourceLoader() { return this.resourceLoader; } @@ -141,8 +138,7 @@ public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { } @Override - @Nullable - public ClassLoader getBeanClassLoader() { + public @Nullable ClassLoader getBeanClassLoader() { return this.beanClassLoader; } 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 6587479b8f55..181705b16a58 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 @@ -31,6 +31,8 @@ import java.util.function.Predicate; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeansException; @@ -69,7 +71,6 @@ import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -115,27 +116,22 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport implements ConfigurableBeanFactory { /** Parent bean factory, for bean inheritance support. */ - @Nullable - private BeanFactory parentBeanFactory; + private @Nullable BeanFactory parentBeanFactory; /** ClassLoader to resolve bean class names with, if necessary. */ - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** ClassLoader to temporarily resolve bean class names with, if necessary. */ - @Nullable - private ClassLoader tempClassLoader; + private @Nullable ClassLoader tempClassLoader; /** Whether to cache bean metadata or rather reobtain it for every access. */ private boolean cacheBeanMetadata = true; /** Resolution strategy for expressions in bean definition values. */ - @Nullable - private BeanExpressionResolver beanExpressionResolver; + private @Nullable BeanExpressionResolver beanExpressionResolver; /** Spring ConversionService to use instead of PropertyEditors. */ - @Nullable - private ConversionService conversionService; + private @Nullable ConversionService conversionService; /** Default PropertyEditorRegistrars to apply to the beans of this factory. */ private final Set defaultEditorRegistrars = new LinkedHashSet<>(4); @@ -147,8 +143,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp private final Map, Class> customEditors = new HashMap<>(4); /** A custom TypeConverter to use, overriding the default PropertyEditor mechanism. */ - @Nullable - private TypeConverter typeConverter; + private @Nullable TypeConverter typeConverter; /** String resolvers to apply, for example, to annotation attribute values. */ private final List embeddedValueResolvers = new CopyOnWriteArrayList<>(); @@ -157,8 +152,7 @@ public abstract class AbstractBeanFactory extends FactoryBeanRegistrySupport imp private final List beanPostProcessors = new BeanPostProcessorCacheAwareList(); /** Cache of pre-filtered post-processors. */ - @Nullable - private BeanPostProcessorCache beanPostProcessorCache; + private @Nullable BeanPostProcessorCache beanPostProcessorCache; /** Map from scope identifier String to corresponding Scope. */ private final Map scopes = new LinkedHashMap<>(8); @@ -208,7 +202,7 @@ public T getBean(String name, Class requiredType) throws BeansException { } @Override - public Object getBean(String name, Object... args) throws BeansException { + public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException { return doGetBean(name, null, args, false); } @@ -221,7 +215,7 @@ public Object getBean(String name, Object... args) throws BeansException { * @return an instance of the bean * @throws BeansException if the bean could not be created */ - public T getBean(String name, @Nullable Class requiredType, @Nullable Object... args) + public T getBean(String name, @Nullable Class requiredType, @Nullable Object @Nullable ... args) throws BeansException { return doGetBean(name, requiredType, args, false); @@ -240,7 +234,7 @@ public T getBean(String name, @Nullable Class requiredType, @Nullable Obj */ @SuppressWarnings("unchecked") protected T doGetBean( - String name, @Nullable Class requiredType, @Nullable Object[] args, boolean typeCheckOnly) + String name, @Nullable Class requiredType, @Nullable Object @Nullable [] args, boolean typeCheckOnly) throws BeansException { String beanName = transformedBeanName(name); @@ -706,14 +700,12 @@ public boolean isTypeMatch(String name, Class typeToMatch) throws NoSuchBeanD } @Override - @Nullable - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { return getType(name, true); } @Override - @Nullable - public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { String beanName = transformedBeanName(name); // Check manually registered singletons. @@ -770,16 +762,16 @@ else if (BeanFactoryUtils.isFactoryDereference(name)) { 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)) { @@ -801,8 +793,7 @@ public String[] getAliases(String name) { //--------------------------------------------------------------------- @Override - @Nullable - public BeanFactory getParentBeanFactory() { + public @Nullable BeanFactory getParentBeanFactory() { return this.parentBeanFactory; } @@ -835,8 +826,7 @@ public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { } @Override - @Nullable - public ClassLoader getBeanClassLoader() { + public @Nullable ClassLoader getBeanClassLoader() { return this.beanClassLoader; } @@ -846,8 +836,7 @@ public void setTempClassLoader(@Nullable ClassLoader tempClassLoader) { } @Override - @Nullable - public ClassLoader getTempClassLoader() { + public @Nullable ClassLoader getTempClassLoader() { return this.tempClassLoader; } @@ -867,8 +856,7 @@ public void setBeanExpressionResolver(@Nullable BeanExpressionResolver resolver) } @Override - @Nullable - public BeanExpressionResolver getBeanExpressionResolver() { + public @Nullable BeanExpressionResolver getBeanExpressionResolver() { return this.beanExpressionResolver; } @@ -878,8 +866,7 @@ public void setConversionService(@Nullable ConversionService conversionService) } @Override - @Nullable - public ConversionService getConversionService() { + public @Nullable ConversionService getConversionService() { return this.conversionService; } @@ -929,8 +916,7 @@ public void setTypeConverter(TypeConverter typeConverter) { * Return the custom TypeConverter to use, if any. * @return the custom TypeConverter, or {@code null} if none specified */ - @Nullable - protected TypeConverter getCustomTypeConverter() { + protected @Nullable TypeConverter getCustomTypeConverter() { return this.typeConverter; } @@ -961,8 +947,7 @@ public boolean hasEmbeddedValueResolver() { } @Override - @Nullable - public String resolveEmbeddedValue(@Nullable String value) { + public @Nullable String resolveEmbeddedValue(@Nullable String value) { if (value == null) { return null; } @@ -1097,8 +1082,7 @@ public String[] getRegisteredScopeNames() { } @Override - @Nullable - public Scope getRegisteredScope(String scopeName) { + public @Nullable Scope getRegisteredScope(String scopeName) { Assert.notNull(scopeName, "Scope identifier must not be null"); return this.scopes.get(scopeName); } @@ -1153,7 +1137,7 @@ public void copyConfigurationFrom(ConfigurableBeanFactory otherFactory) { 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. @@ -1292,7 +1276,7 @@ protected String transformedBeanName(String name) { */ 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; @@ -1474,7 +1458,7 @@ protected RootBeanDefinition getMergedBeanDefinition( // 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) { @@ -1503,6 +1487,18 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, } } + /** + * 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. @@ -1510,7 +1506,7 @@ private void copyRelevantMergedBeanDefinitionCaches(RootBeanDefinition previous, * @param beanName the name of the bean * @param args the arguments for bean creation, if any */ - protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { + protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object @Nullable [] args) { if (mbd.isAbstract()) { throw new BeanIsAbstractException(beanName); } @@ -1555,8 +1551,7 @@ public void clearMetadataCache() { * @return the resolved bean class (or {@code null} if none) * @throws CannotLoadBeanClassException if we failed to load the class */ - @Nullable - protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Class... typesToMatch) + protected @Nullable Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Class... typesToMatch) throws CannotLoadBeanClassException { try { @@ -1581,8 +1576,7 @@ protected Class resolveBeanClass(RootBeanDefinition mbd, String beanName, Cla } } - @Nullable - private Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToMatch) + private @Nullable Class doResolveBeanClass(RootBeanDefinition mbd, Class... typesToMatch) throws ClassNotFoundException { ClassLoader beanClassLoader = getBeanClassLoader(); @@ -1649,8 +1643,7 @@ else if (evaluated instanceof String name) { * @return the resolved value * @see #setBeanExpressionResolver */ - @Nullable - protected Object evaluateBeanDefinitionString(@Nullable String value, @Nullable BeanDefinition beanDefinition) { + protected @Nullable Object evaluateBeanDefinitionString(@Nullable String value, @Nullable BeanDefinition beanDefinition) { if (this.beanExpressionResolver == null) { return value; } @@ -1681,8 +1674,7 @@ protected Object evaluateBeanDefinitionString(@Nullable String value, @Nullable * (also signals that the returned {@code Class} will never be exposed to application code) * @return the type of the bean, or {@code null} if not predictable */ - @Nullable - protected Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { + protected @Nullable Class predictBeanType(String beanName, RootBeanDefinition mbd, Class... typesToMatch) { Class targetType = mbd.getTargetType(); if (targetType != null) { return targetType; @@ -1999,7 +1991,7 @@ protected void registerDisposableBeanIfNecessary(String beanName, Object bean, R * @return a new instance of the bean * @throws BeanCreationException if the bean could not be created */ - protected abstract Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args) + protected abstract Object createBean(String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] args) throws BeanCreationException; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java index c078572af5ab..a33573cb3645 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireCandidateResolver.java @@ -16,10 +16,11 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.DependencyDescriptor; -import org.springframework.lang.Nullable; /** * Strategy interface for determining whether a specific bean definition @@ -79,8 +80,7 @@ default boolean hasQualifier(DependencyDescriptor descriptor) { * @return the qualifier value, if any * @since 6.2 */ - @Nullable - default String getSuggestedName(DependencyDescriptor descriptor) { + default @Nullable String getSuggestedName(DependencyDescriptor descriptor) { return null; } @@ -92,8 +92,7 @@ default String getSuggestedName(DependencyDescriptor descriptor) { * or {@code null} if none found * @since 3.0 */ - @Nullable - default Object getSuggestedValue(DependencyDescriptor descriptor) { + default @Nullable Object getSuggestedValue(DependencyDescriptor descriptor) { return null; } @@ -107,8 +106,7 @@ default Object getSuggestedValue(DependencyDescriptor descriptor) { * or {@code null} if straight resolution is to be performed * @since 4.0 */ - @Nullable - default Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { + default @Nullable Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { return null; } @@ -121,8 +119,7 @@ default Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor * @return the lazy resolution proxy class for the dependency target, if any * @since 6.0 */ - @Nullable - default Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { + default @Nullable Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java index d23cda6e322e..957bbead7401 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/AutowireUtils.java @@ -32,13 +32,14 @@ import java.util.Comparator; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.TypedStringValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -175,7 +176,7 @@ public static Object resolveAutowiringValue(Object autowiringValue, Class req * @since 3.2.5 */ public static Class resolveReturnTypeForFactoryMethod( - Method method, Object[] args, @Nullable ClassLoader classLoader) { + Method method, @Nullable Object[] args, @Nullable ClassLoader classLoader) { Assert.notNull(method, "Method must not be null"); Assert.notNull(args, "Argument array must not be null"); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java index d82d66bd75c0..627059921c26 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionBuilder.java @@ -18,11 +18,12 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.AutowiredPropertyMarker; import org.springframework.beans.factory.config.BeanDefinitionCustomizer; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java index eb76dd9d13d8..ef70bb65fa23 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionDefaults.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -29,18 +30,15 @@ */ public class BeanDefinitionDefaults { - @Nullable - private Boolean lazyInit; + private @Nullable Boolean lazyInit; private int autowireMode = AbstractBeanDefinition.AUTOWIRE_NO; private int dependencyCheck = AbstractBeanDefinition.DEPENDENCY_CHECK_NONE; - @Nullable - private String initMethodName; + private @Nullable String initMethodName; - @Nullable - private String destroyMethodName; + private @Nullable String destroyMethodName; /** @@ -68,8 +66,7 @@ public boolean isLazyInit() { * @return the lazy-init flag if explicitly set, or {@code null} otherwise * @since 5.2 */ - @Nullable - public Boolean getLazyInit() { + public @Nullable Boolean getLazyInit() { return this.lazyInit; } @@ -124,8 +121,7 @@ public void setInitMethodName(@Nullable String initMethodName) { /** * Return the name of the default initializer method. */ - @Nullable - public String getInitMethodName() { + public @Nullable String getInitMethodName() { return this.initMethodName; } @@ -143,8 +139,7 @@ public void setDestroyMethodName(@Nullable String destroyMethodName) { /** * Return the name of the default destroy method. */ - @Nullable - public String getDestroyMethodName() { + public @Nullable String getDestroyMethodName() { return this.destroyMethodName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java index a815db479f92..6ccc96a08af6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionOverrideException.java @@ -18,7 +18,6 @@ import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.lang.NonNull; /** * Subclass of {@link BeanDefinitionStoreException} indicating an invalid override @@ -75,7 +74,6 @@ public BeanDefinitionOverrideException( * Return the description of the resource that the bean definition came from. */ @Override - @NonNull public String getResourceDescription() { return String.valueOf(super.getResourceDescription()); } @@ -84,7 +82,6 @@ public String getResourceDescription() { * Return the name of the bean. */ @Override - @NonNull public String getBeanName() { return String.valueOf(super.getBeanName()); } 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 8441abbc7664..2fe442ef738a 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. @@ -16,10 +16,11 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; /** * Simple interface for bean definition readers that specifies load methods with @@ -29,10 +30,6 @@ * 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 @@ -63,8 +60,7 @@ public interface BeanDefinitionReader { * @see #loadBeanDefinitions(String) * @see org.springframework.core.io.support.ResourcePatternResolver */ - @Nullable - ResourceLoader getResourceLoader(); + @Nullable ResourceLoader getResourceLoader(); /** * Return the class loader to use for bean classes. @@ -72,8 +68,7 @@ public interface BeanDefinitionReader { * but rather to just register bean definitions with class names, * with the corresponding classes to be resolved later (or never). */ - @Nullable - ClassLoader getBeanClassLoader(); + @Nullable ClassLoader getBeanClassLoader(); /** * Return the {@link BeanNameGenerator} to use for anonymous beans diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java index b02687792e15..678e37d948b5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionReaderUtils.java @@ -16,11 +16,12 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionResource.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionResource.java index f59222a96211..54f61ada5fff 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionResource.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionResource.java @@ -20,9 +20,10 @@ import java.io.IOException; import java.io.InputStream; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.io.AbstractResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java index c3efcdcc0b2d..3c91711fcdf7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanDefinitionValueResolver.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.util.Set; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; @@ -41,7 +43,6 @@ import org.springframework.beans.factory.config.RuntimeBeanNameReference; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.config.TypedStringValue; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -127,8 +128,7 @@ public BeanDefinitionValueResolver(AbstractAutowireCapableBeanFactory beanFactor * @param value the value object to resolve * @return the resolved object */ - @Nullable - public Object resolveValueIfNecessary(Object argName, @Nullable Object value) { + public @Nullable Object resolveValueIfNecessary(Object argName, @Nullable Object value) { // We must check each value to see whether it requires a runtime reference // to another bean to be resolved. if (value instanceof RuntimeBeanReference ref) { @@ -268,8 +268,7 @@ public T resolveInnerBean(@Nullable String innerBeanName, BeanDefinition inn * @param value the candidate value (may be an expression) * @return the resolved value */ - @Nullable - protected Object evaluate(TypedStringValue value) { + protected @Nullable Object evaluate(TypedStringValue value) { Object result = doEvaluate(value.getValue()); if (!ObjectUtils.nullSafeEquals(result, value.getValue())) { value.setDynamic(); @@ -282,14 +281,13 @@ protected Object evaluate(TypedStringValue value) { * @param value the original value (may be an expression) * @return the resolved value if necessary, or the original value */ - @Nullable - protected Object evaluate(@Nullable Object value) { + protected @Nullable Object evaluate(@Nullable Object value) { if (value instanceof String str) { return doEvaluate(str); } else if (value instanceof String[] values) { boolean actuallyResolved = false; - Object[] resolvedValues = new Object[values.length]; + @Nullable Object[] resolvedValues = new Object[values.length]; for (int i = 0; i < values.length; i++) { String originalValue = values[i]; Object resolvedValue = doEvaluate(originalValue); @@ -310,8 +308,7 @@ else if (value instanceof String[] values) { * @param value the original value (may be an expression) * @return the resolved value if necessary, or the original String value */ - @Nullable - private Object doEvaluate(@Nullable String value) { + private @Nullable Object doEvaluate(@Nullable String value) { return this.beanFactory.evaluateBeanDefinitionString(value, this.beanDefinition); } @@ -322,8 +319,7 @@ private Object doEvaluate(@Nullable String value) { * @throws ClassNotFoundException if the specified type cannot be resolved * @see TypedStringValue#resolveTargetType */ - @Nullable - protected Class resolveTargetType(TypedStringValue value) throws ClassNotFoundException { + protected @Nullable Class resolveTargetType(TypedStringValue value) throws ClassNotFoundException { if (value.hasTargetType()) { return value.getTargetType(); } @@ -333,8 +329,7 @@ protected Class resolveTargetType(TypedStringValue value) throws ClassNotFoun /** * Resolve a reference to another bean in the factory. */ - @Nullable - private Object resolveReference(Object argName, RuntimeBeanReference ref) { + private @Nullable Object resolveReference(Object argName, RuntimeBeanReference ref) { try { Object bean; Class beanType = ref.getBeanType(); @@ -385,8 +380,7 @@ private Object resolveReference(Object argName, RuntimeBeanReference ref) { * @param mbd the merged bean definition for the inner bean * @return the resolved inner bean instance */ - @Nullable - private Object resolveInnerBeanValue(Object argName, String innerBeanName, RootBeanDefinition mbd) { + private @Nullable Object resolveInnerBeanValue(Object argName, String innerBeanName, RootBeanDefinition mbd) { try { // Check given bean name whether it is unique. If not already unique, // add counter - increasing the counter until the name is unique. diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java new file mode 100644 index 000000000000..e3c0ad363b1b --- /dev/null +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/BeanRegistryAdapter.java @@ -0,0 +1,285 @@ +/* + * Copyright 2002-2025 the original author 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.beans.factory.support; + +import java.lang.reflect.Constructor; +import java.util.function.Consumer; +import java.util.function.Function; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.beans.factory.ListableBeanFactory; +import org.springframework.beans.factory.ObjectProvider; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.Environment; +import org.springframework.util.Assert; +import org.springframework.util.MultiValueMap; + +/** + * {@link BeanRegistry} implementation that delegates to + * {@link BeanDefinitionRegistry} and {@link ListableBeanFactory}. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public class BeanRegistryAdapter implements BeanRegistry { + + private final BeanDefinitionRegistry beanRegistry; + + private final ListableBeanFactory beanFactory; + + private final Environment environment; + + private final Class beanRegistrarClass; + + private final @Nullable MultiValueMap customizers; + + + public BeanRegistryAdapter(DefaultListableBeanFactory beanFactory, Environment environment, + Class beanRegistrarClass) { + + this(beanFactory, beanFactory, environment, beanRegistrarClass, null); + } + + public BeanRegistryAdapter(BeanDefinitionRegistry beanRegistry, ListableBeanFactory beanFactory, + Environment environment, Class beanRegistrarClass) { + + this(beanRegistry, beanFactory, environment, beanRegistrarClass, null); + } + + public BeanRegistryAdapter(BeanDefinitionRegistry beanRegistry, ListableBeanFactory beanFactory, + Environment environment, Class beanRegistrarClass, + @Nullable MultiValueMap customizers) { + + this.beanRegistry = beanRegistry; + this.beanFactory = beanFactory; + this.environment = environment; + this.beanRegistrarClass = beanRegistrarClass; + this.customizers = customizers; + } + + + @Override + public void registerAlias(String name, String alias) { + this.beanRegistry.registerAlias(name, alias); + } + + @Override + public String registerBean(Class beanClass) { + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); + registerBean(beanName, beanClass); + return beanName; + } + + @Override + public String registerBean(Class beanClass, Consumer> customizer) { + String beanName = BeanDefinitionReaderUtils.uniqueBeanName(beanClass.getName(), this.beanRegistry); + registerBean(beanName, beanClass, customizer); + return beanName; + } + + @Override + public void registerBean(String name, Class beanClass) { + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer customizer : this.customizers.get(name)) { + customizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + @Override + public void registerBean(String name, Class beanClass, Consumer> spec) { + BeanRegistrarBeanDefinition beanDefinition = new BeanRegistrarBeanDefinition(beanClass, this.beanRegistrarClass); + spec.accept(new BeanSpecAdapter<>(beanDefinition, this.beanFactory)); + if (this.customizers != null && this.customizers.containsKey(name)) { + for (BeanDefinitionCustomizer customizer : this.customizers.get(name)) { + customizer.customize(beanDefinition); + } + } + this.beanRegistry.registerBeanDefinition(name, beanDefinition); + } + + @Override + public void register(BeanRegistrar registrar) { + Assert.notNull(registrar, "'registrar' must not be null"); + registrar.register(this, this.environment); + } + + + /** + * {@link RootBeanDefinition} subclass for {@code #registerBean} based + * registrations with constructors resolution match{@link BeanUtils#getResolvableConstructor} + * behavior. It also sets the bean registrar class as the source. + */ + @SuppressWarnings("serial") + private static class BeanRegistrarBeanDefinition extends RootBeanDefinition { + + public BeanRegistrarBeanDefinition(Class beanClass, Class beanRegistrarClass) { + super(beanClass); + this.setSource(beanRegistrarClass); + this.setAttribute("aotProcessingIgnoreRegistration", true); + } + + public BeanRegistrarBeanDefinition(BeanRegistrarBeanDefinition original) { + super(original); + } + + @Override + public Constructor @Nullable [] getPreferredConstructors() { + if (this.getInstanceSupplier() != null) { + return null; + } + try { + return new Constructor[] { BeanUtils.getResolvableConstructor(getBeanClass()) }; + } + catch (IllegalStateException ex) { + return null; + } + } + + @Override + public RootBeanDefinition cloneBeanDefinition() { + return new BeanRegistrarBeanDefinition(this); + } + } + + + private static class BeanSpecAdapter implements Spec { + + private final RootBeanDefinition beanDefinition; + + private final ListableBeanFactory beanFactory; + + public BeanSpecAdapter(RootBeanDefinition beanDefinition, ListableBeanFactory beanFactory) { + this.beanDefinition = beanDefinition; + this.beanFactory = beanFactory; + } + + @Override + public Spec backgroundInit() { + this.beanDefinition.setBackgroundInit(true); + return this; + } + + @Override + public Spec fallback() { + this.beanDefinition.setFallback(true); + return this; + } + + @Override + public Spec primary() { + this.beanDefinition.setPrimary(true); + return this; + } + + @Override + public Spec description(String description) { + this.beanDefinition.setDescription(description); + return this; + } + + @Override + public Spec infrastructure() { + this.beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + return this; + } + + @Override + public Spec lazyInit() { + this.beanDefinition.setLazyInit(true); + return this; + } + + @Override + public Spec notAutowirable() { + this.beanDefinition.setAutowireCandidate(false); + return this; + } + + @Override + public Spec order(int order) { + this.beanDefinition.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, order); + return this; + } + + @Override + public Spec prototype() { + this.beanDefinition.setScope(BeanDefinition.SCOPE_PROTOTYPE); + return this; + } + + @Override + public Spec supplier(Function supplier) { + this.beanDefinition.setInstanceSupplier(() -> + supplier.apply(new SupplierContextAdapter(this.beanFactory))); + return this; + } + + @Override + public Spec targetType(ParameterizedTypeReference targetType) { + this.beanDefinition.setTargetType(ResolvableType.forType(targetType)); + return this; + } + + @Override + public Spec targetType(ResolvableType targetType) { + this.beanDefinition.setTargetType(targetType); + return this; + } + } + + + private static class SupplierContextAdapter implements SupplierContext { + + private final ListableBeanFactory beanFactory; + + public SupplierContextAdapter(ListableBeanFactory beanFactory) { + this.beanFactory = beanFactory; + } + + @Override + public T bean(Class requiredType) throws BeansException { + return this.beanFactory.getBean(requiredType); + } + + @Override + public T bean(String name, Class requiredType) throws BeansException { + return this.beanFactory.getBean(name, requiredType); + } + + @Override + public ObjectProvider beanProvider(Class requiredType) { + return this.beanFactory.getBeanProvider(requiredType); + } + + @Override + public ObjectProvider beanProvider(ResolvableType requiredType) { + return this.beanFactory.getBeanProvider(requiredType); + } + } + +} 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 4c1f826f56a0..baa679b599e2 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. @@ -21,7 +21,9 @@ 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; @@ -36,7 +38,6 @@ import org.springframework.cglib.proxy.MethodProxy; import org.springframework.cglib.proxy.NoOp; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -153,7 +154,7 @@ public Class createEnhancedSubclass(RootBeanDefinition beanDefinition) { Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(beanDefinition.getBeanClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); if (this.owner instanceof ConfigurableBeanFactory cbf) { ClassLoader cl = cbf.getBeanClassLoader(); enhancer.setStrategy(new ClassLoaderAwareGeneratorStrategy(cl)); @@ -241,8 +242,7 @@ public LookupOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanFa } @Override - @Nullable - public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { + public @Nullable Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { // Cast is safe, as CallbackFilter filters are used selectively. LookupOverride lo = (LookupOverride) getBeanDefinition().getMethodOverrides().getOverride(method); Assert.state(lo != null, "LookupOverride not found"); @@ -276,18 +276,15 @@ public ReplaceOverrideMethodInterceptor(RootBeanDefinition beanDefinition, BeanF this.owner = owner; } - @Nullable @Override - public Object intercept(Object obj, Method method, Object[] args, MethodProxy mp) throws Throwable { + 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)); } - @Nullable - private T processReturnType(Method method, @Nullable T returnValue) { + private @Nullable T processReturnType(Method method, @Nullable T returnValue) { Class returnType = method.getReturnType(); if (returnValue == null && returnType != void.class && returnType.isPrimitive()) { throw new IllegalStateException( diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java index 5f15616d95a1..6cb7c13f0d72 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ChildBeanDefinition.java @@ -16,9 +16,10 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -46,8 +47,7 @@ @SuppressWarnings("serial") public class ChildBeanDefinition extends AbstractBeanDefinition { - @Nullable - private String parentName; + private @Nullable String parentName; /** @@ -136,8 +136,7 @@ public void setParentName(@Nullable String parentName) { } @Override - @Nullable - public String getParentName() { + public @Nullable String getParentName() { return this.parentName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java index 39d0323db4a1..e719ddd284ab 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ConstructorResolver.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 java.util.function.Supplier; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.BeanUtils; @@ -68,7 +69,6 @@ import org.springframework.core.NamedThreadLocal; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -131,16 +131,16 @@ public ConstructorResolver(AbstractAutowireCapableBeanFactory beanFactory) { * or {@code null} if none (-> use constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, - @Nullable Constructor[] chosenCtors, @Nullable Object[] explicitArgs) { + Constructor @Nullable [] chosenCtors, @Nullable Object @Nullable [] explicitArgs) { BeanWrapperImpl bw = new BeanWrapperImpl(); this.beanFactory.initBeanWrapper(bw); Constructor constructorToUse = null; ArgumentsHolder argsHolderToUse = null; - Object[] argsToUse = null; + @Nullable Object[] argsToUse = null; if (explicitArgs != null) { argsToUse = explicitArgs; @@ -227,7 +227,7 @@ public BeanWrapper autowireConstructor(String beanName, RootBeanDefinition mbd, Class[] paramTypes = candidate.getParameterTypes(); if (resolvedValues != null) { try { - String[] paramNames = null; + @Nullable String[] paramNames = null; if (resolvedValues.containsNamedArgument()) { paramNames = ConstructorPropertiesChecker.evaluate(candidate, parameterCount); if (paramNames == null) { @@ -393,9 +393,9 @@ private boolean isStaticCandidate(Method method, Class factoryClass) { * method, or {@code null} if none (-> use constructor argument values from bean definition) * @return a BeanWrapper for the new instance */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public BeanWrapper instantiateUsingFactoryMethod( - String beanName, RootBeanDefinition mbd, @Nullable Object[] explicitArgs) { + String beanName, RootBeanDefinition mbd, @Nullable Object @Nullable [] explicitArgs) { BeanWrapperImpl bw = new BeanWrapperImpl(); this.beanFactory.initBeanWrapper(bw); @@ -431,13 +431,13 @@ public BeanWrapper instantiateUsingFactoryMethod( Method factoryMethodToUse = null; ArgumentsHolder argsHolderToUse = null; - Object[] argsToUse = null; + @Nullable Object[] argsToUse = null; if (explicitArgs != null) { argsToUse = explicitArgs; } else { - Object[] argsToResolve = null; + @Nullable Object[] argsToResolve = null; synchronized (mbd.constructorArgumentLock) { factoryMethodToUse = (Method) mbd.resolvedConstructorOrFactoryMethod; if (factoryMethodToUse != null && mbd.constructorArgumentsResolved) { @@ -536,7 +536,7 @@ public BeanWrapper instantiateUsingFactoryMethod( else { // Resolved constructor arguments: type conversion and/or autowiring necessary. try { - String[] paramNames = null; + @Nullable String[] paramNames = null; if (resolvedValues != null && resolvedValues.containsNamedArgument()) { ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer(); if (pnd != null) { @@ -624,7 +624,7 @@ else if (void.class == factoryMethodToUse.getReturnType()) { "Invalid factory method '" + mbd.getFactoryMethodName() + "' on class [" + factoryClass.getName() + "]: needs to have a non-void return type!"); } - else if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(factoryMethodToUse)) { + else if (KotlinDetector.isSuspendingFunction(factoryMethodToUse)) { throw new BeanCreationException(mbd.getResourceDescription(), beanName, "Invalid factory method '" + mbd.getFactoryMethodName() + "' on class [" + factoryClass.getName() + "]: suspending functions are not supported!"); @@ -647,7 +647,7 @@ else if (ambiguousFactoryMethods != null) { } private Object instantiate(String beanName, RootBeanDefinition mbd, - @Nullable Object factoryBean, Method factoryMethod, Object[] args) { + @Nullable Object factoryBean, Method factoryMethod, @Nullable Object[] args) { try { return this.beanFactory.getInstantiationStrategy().instantiate( @@ -719,7 +719,7 @@ private int resolveConstructorArguments(String beanName, RootBeanDefinition mbd, */ private ArgumentsHolder createArgumentArray( String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues, - BeanWrapper bw, Class[] paramTypes, @Nullable String[] paramNames, Executable executable, + BeanWrapper bw, Class[] paramTypes, @Nullable String @Nullable [] paramNames, Executable executable, boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException { TypeConverter customConverter = this.beanFactory.getCustomTypeConverter(); @@ -814,7 +814,7 @@ private ArgumentsHolder createArgumentArray( /** * Resolve the prepared arguments stored in the given bean definition. */ - private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw, + private @Nullable Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mbd, BeanWrapper bw, Executable executable, Object[] argsToResolve) { TypeConverter customConverter = this.beanFactory.getCustomTypeConverter(); @@ -823,7 +823,7 @@ private Object[] resolvePreparedArguments(String beanName, RootBeanDefinition mb new BeanDefinitionValueResolver(this.beanFactory, beanName, mbd, converter); Class[] paramTypes = executable.getParameterTypes(); - Object[] resolvedArgs = new Object[argsToResolve.length]; + @Nullable Object[] resolvedArgs = new Object[argsToResolve.length]; for (int argIndex = 0; argIndex < argsToResolve.length; argIndex++) { Object argValue = argsToResolve[argIndex]; Class paramType = paramTypes[argIndex]; @@ -897,8 +897,7 @@ private Constructor getUserDeclaredConstructor(Constructor constructor) { /** * Resolve the specified argument which is supposed to be autowired. */ - @Nullable - Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramType, String beanName, + @Nullable Object resolveAutowiredArgument(DependencyDescriptor descriptor, Class paramType, String beanName, @Nullable Set autowiredBeanNames, TypeConverter typeConverter, boolean fallback) { if (InjectionPoint.class.isAssignableFrom(paramType)) { @@ -1041,8 +1040,7 @@ private ResolvableType determineParameterValueType(RootBeanDefinition mbd, Value return ResolvableType.forInstance(value); } - @Nullable - private Constructor resolveConstructor(String beanName, RootBeanDefinition mbd, + private @Nullable Constructor resolveConstructor(String beanName, RootBeanDefinition mbd, Supplier beanType, List valueTypes) { Class type = ClassUtils.getUserClass(beanType.get().toClass()); @@ -1089,8 +1087,7 @@ private Constructor resolveConstructor(String beanName, RootBeanDefinition mb return (typeConversionFallbackMatches.size() == 1 ? typeConversionFallbackMatches.get(0) : null); } - @Nullable - private Method resolveFactoryMethod(String beanName, RootBeanDefinition mbd, List valueTypes) { + private @Nullable Method resolveFactoryMethod(String beanName, RootBeanDefinition mbd, List valueTypes) { if (mbd.isFactoryMethodUnique) { Method resolvedFactoryMethod = mbd.getResolvedFactoryMethod(); if (resolvedFactoryMethod != null) { @@ -1149,8 +1146,7 @@ else if (candidates.size() > 1) { return null; } - @Nullable - private Method resolveFactoryMethod(List executables, + private @Nullable Method resolveFactoryMethod(List executables, Function> parameterTypesFactory, List valueTypes) { @@ -1257,8 +1253,7 @@ private Predicate isSimpleValueType(ResolvableType valueType) { BeanUtils.isSimpleValueType(valueType.toClass())); } - @Nullable - private Class getFactoryBeanClass(String beanName, RootBeanDefinition mbd) { + private @Nullable Class getFactoryBeanClass(String beanName, RootBeanDefinition mbd) { Class beanClass = this.beanFactory.resolveBeanClass(mbd, beanName); return (beanClass != null && FactoryBean.class.isAssignableFrom(beanClass) ? beanClass : null); } @@ -1288,8 +1283,7 @@ static InjectionPoint setCurrentInjectionPoint(@Nullable InjectionPoint injectio * This variant adds a lenient fallback to the default constructor if available, similar to * {@link org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor#determineCandidateConstructors}. */ - @Nullable - static Constructor[] determinePreferredConstructors(Class clazz) { + static Constructor @Nullable [] determinePreferredConstructors(Class clazz) { Constructor primaryCtor = BeanUtils.findPrimaryConstructor(clazz); Constructor defaultCtor; @@ -1337,11 +1331,11 @@ else if (ctors.length == 0) { */ private static class ArgumentsHolder { - public final Object[] rawArguments; + public final @Nullable Object[] rawArguments; - public final Object[] arguments; + public final @Nullable Object[] arguments; - public final Object[] preparedArguments; + public final @Nullable Object[] preparedArguments; public boolean resolveNecessary = false; @@ -1351,7 +1345,7 @@ public ArgumentsHolder(int size) { this.preparedArguments = new Object[size]; } - public ArgumentsHolder(Object[] args) { + public ArgumentsHolder(@Nullable Object[] args) { this.rawArguments = args; this.arguments = args; this.preparedArguments = args; @@ -1401,8 +1395,7 @@ public void storeCache(RootBeanDefinition mbd, Executable constructorOrFactoryMe */ private static class ConstructorPropertiesChecker { - @Nullable - public static String[] evaluate(Constructor candidate, int paramCount) { + public static String @Nullable [] evaluate(Constructor candidate, int paramCount) { ConstructorProperties cp = candidate.getAnnotation(ConstructorProperties.class); if (cp != null) { String[] names = cp.value(); @@ -1427,8 +1420,7 @@ public static String[] evaluate(Constructor candidate, int paramCount) { @SuppressWarnings("serial") private static class ConstructorDependencyDescriptor extends DependencyDescriptor { - @Nullable - private volatile String shortcut; + private volatile @Nullable String shortcut; public ConstructorDependencyDescriptor(MethodParameter methodParameter, boolean required) { super(methodParameter, required); @@ -1443,8 +1435,7 @@ public boolean hasShortcut() { } @Override - @Nullable - public Object resolveShortcut(BeanFactory beanFactory) { + public @Nullable Object resolveShortcut(BeanFactory beanFactory) { String shortcut = this.shortcut; return (shortcut != null ? beanFactory.getBean(shortcut, getDependencyType()) : null); } 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 57c1d1a38faa..54a4930535ca 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 @@ -47,6 +47,7 @@ import java.util.stream.Stream; import jakarta.inject.Provider; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.TypeConverter; @@ -76,13 +77,13 @@ import org.springframework.core.OrderComparator; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; +import org.springframework.core.SpringProperties; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.log.LogMessage; import org.springframework.core.metrics.StartupStep; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -128,8 +129,21 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFactory implements ConfigurableListableBeanFactory, BeanDefinitionRegistry, Serializable { - @Nullable - private static Class jakartaInjectProviderClass; + /** + * 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 @Nullable Class jakartaInjectProviderClass; static { try { @@ -147,23 +161,22 @@ 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. */ - @Nullable - private String serializationId; + private @Nullable String serializationId; /** Whether to allow re-registration of a different definition with the same name. */ - @Nullable - private Boolean allowBeanDefinitionOverriding; + private @Nullable Boolean allowBeanDefinitionOverriding; /** Whether to allow eager class loading even for lazy-init beans. */ private boolean allowEagerClassLoading = true; - @Nullable - private Executor bootstrapExecutor; + private @Nullable Executor bootstrapExecutor; /** Optional OrderComparator for dependency Lists and arrays. */ - @Nullable - private Comparator dependencyComparator; + private @Nullable Comparator dependencyComparator; /** Resolver to use for checking if a bean definition is an autowire candidate. */ private AutowireCandidateResolver autowireCandidateResolver = SimpleAutowireCandidateResolver.INSTANCE; @@ -193,13 +206,13 @@ public class DefaultListableBeanFactory extends AbstractAutowireCapableBeanFacto private volatile Set manualSingletonNames = new LinkedHashSet<>(16); /** Cached array of bean definition names in case of frozen configuration. */ - @Nullable - private volatile String[] frozenBeanDefinitionNames; + private volatile String @Nullable [] frozenBeanDefinitionNames; /** 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"); @@ -240,8 +253,7 @@ else if (this.serializationId != null) { * to be deserialized from this id back into the BeanFactory object, if needed. * @since 4.1.2 */ - @Nullable - public String getSerializationId() { + public @Nullable String getSerializationId() { return this.serializationId; } @@ -294,8 +306,7 @@ public void setBootstrapExecutor(@Nullable Executor bootstrapExecutor) { } @Override - @Nullable - public Executor getBootstrapExecutor() { + public @Nullable Executor getBootstrapExecutor() { return this.bootstrapExecutor; } @@ -313,8 +324,7 @@ public void setDependencyComparator(@Nullable Comparator dependencyCompa * Return the dependency comparator for this BeanFactory (may be {@code null}). * @since 4.0 */ - @Nullable - public Comparator getDependencyComparator() { + public @Nullable Comparator getDependencyComparator() { return this.dependencyComparator; } @@ -366,7 +376,7 @@ public T getBean(Class requiredType) throws BeansException { @SuppressWarnings("unchecked") @Override - public T getBean(Class requiredType, @Nullable Object... args) throws BeansException { + public T getBean(Class requiredType, @Nullable Object @Nullable ... args) throws BeansException { Assert.notNull(requiredType, "Required type must not be null"); Object resolved = resolveBean(ResolvableType.forRawClass(requiredType), args, false); if (resolved == null) { @@ -431,7 +441,7 @@ public T getObject() throws BeansException { return resolved; } @Override - public T getObject(Object... args) throws BeansException { + public T getObject(@Nullable Object... args) throws BeansException { T resolved = resolveBean(requiredType, args, false); if (resolved == null) { throw new NoSuchBeanDefinitionException(requiredType); @@ -439,8 +449,7 @@ public T getObject(Object... args) throws BeansException { return resolved; } @Override - @Nullable - public T getIfAvailable() throws BeansException { + public @Nullable T getIfAvailable() throws BeansException { try { return resolveBean(requiredType, null, false); } @@ -462,8 +471,7 @@ public void ifAvailable(Consumer dependencyConsumer) throws BeansException { } } @Override - @Nullable - public T getIfUnique() throws BeansException { + public @Nullable T getIfUnique() throws BeansException { try { return resolveBean(requiredType, null, true); } @@ -487,14 +495,14 @@ public void ifUnique(Consumer dependencyConsumer) throws BeansException { @SuppressWarnings("unchecked") @Override public Stream stream() { - return Arrays.stream(getBeanNamesForTypedStream(requiredType, allowEagerInit)) + return Arrays.stream(beanNamesForStream(requiredType, true, allowEagerInit)) .map(name -> (T) getBean(name)) .filter(bean -> !(bean instanceof NullBean)); } @SuppressWarnings("unchecked") @Override public Stream orderedStream() { - String[] beanNames = getBeanNamesForTypedStream(requiredType, allowEagerInit); + String[] beanNames = beanNamesForStream(requiredType, true, allowEagerInit); if (beanNames.length == 0) { return Stream.empty(); } @@ -510,16 +518,16 @@ public Stream orderedStream() { } @SuppressWarnings("unchecked") @Override - public Stream stream(Predicate> customFilter) { - return Arrays.stream(getBeanNamesForTypedStream(requiredType, allowEagerInit)) + public Stream stream(Predicate> customFilter, boolean includeNonSingletons) { + return Arrays.stream(beanNamesForStream(requiredType, includeNonSingletons, allowEagerInit)) .filter(name -> customFilter.test(getType(name))) .map(name -> (T) getBean(name)) .filter(bean -> !(bean instanceof NullBean)); } @SuppressWarnings("unchecked") @Override - public Stream orderedStream(Predicate> customFilter) { - String[] beanNames = getBeanNamesForTypedStream(requiredType, allowEagerInit); + public Stream orderedStream(Predicate> customFilter, boolean includeNonSingletons) { + String[] beanNames = beanNamesForStream(requiredType, includeNonSingletons, allowEagerInit); if (beanNames.length == 0) { return Stream.empty(); } @@ -537,8 +545,7 @@ public Stream orderedStream(Predicate> customFilter) { }; } - @Nullable - private T resolveBean(ResolvableType requiredType, @Nullable Object[] args, boolean nonUniqueAsNull) { + private @Nullable T resolveBean(ResolvableType requiredType, @Nullable Object @Nullable [] args, boolean nonUniqueAsNull) { NamedBeanHolder namedBean = resolveNamedBean(requiredType, args, nonUniqueAsNull); if (namedBean != null) { return namedBean.getBeanInstance(); @@ -559,8 +566,8 @@ else if (parent != null) { return null; } - private String[] getBeanNamesForTypedStream(ResolvableType requiredType, boolean allowEagerInit) { - return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this, requiredType, true, allowEagerInit); + private String[] beanNamesForStream(ResolvableType requiredType, boolean includeNonSingletons, boolean allowEagerInit) { + return BeanFactoryUtils.beanNamesForTypeIncludingAncestors(this, requiredType, includeNonSingletons, allowEagerInit); } @Override @@ -626,10 +633,15 @@ private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSi } } 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; @@ -775,16 +787,14 @@ public Map getBeansWithAnnotation(Class an } @Override - @Nullable - public A findAnnotationOnBean(String beanName, Class annotationType) + public @Nullable A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException { return findAnnotationOnBean(beanName, annotationType, true); } @Override - @Nullable - public A findAnnotationOnBean( + public @Nullable A findAnnotationOnBean( String beanName, Class annotationType, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { @@ -895,7 +905,7 @@ protected boolean isAutowireCandidate( String beanName, DependencyDescriptor descriptor, AutowireCandidateResolver resolver) throws NoSuchBeanDefinitionException { - String bdName = BeanFactoryUtils.transformedBeanName(beanName); + String bdName = transformedBeanName(beanName); if (containsBeanDefinition(bdName)) { return isAutowireCandidate(beanName, getMergedLocalBeanDefinition(bdName), descriptor, resolver); } @@ -929,7 +939,7 @@ else if (parent instanceof ConfigurableListableBeanFactory clbf) { protected boolean isAutowireCandidate(String beanName, RootBeanDefinition mbd, DependencyDescriptor descriptor, AutowireCandidateResolver resolver) { - String bdName = BeanFactoryUtils.transformedBeanName(beanName); + String bdName = transformedBeanName(beanName); resolveBeanClass(mbd, bdName); if (mbd.isFactoryMethodUnique && mbd.factoryMethodToIntrospect == null) { new ConstructorResolver(this).resolveFactoryMethodIfPossible(mbd); @@ -997,8 +1007,7 @@ protected boolean isBeanEligibleForMetadataCaching(String beanName) { } @Override - @Nullable - protected Object obtainInstanceFromSupplier(Supplier supplier, String beanName, RootBeanDefinition mbd) + protected @Nullable Object obtainInstanceFromSupplier(Supplier supplier, String beanName, RootBeanDefinition mbd) throws Exception { if (supplier instanceof InstanceSupplier instanceSupplier) { @@ -1008,7 +1017,15 @@ protected Object obtainInstanceFromSupplier(Supplier supplier, String beanNam } @Override - protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName, @Nullable Object[] args) { + 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); if (mbd.isBackgroundInit()) { @@ -1019,7 +1036,7 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName } } 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 " + @@ -1029,9 +1046,41 @@ protected void checkMergedBeanDefinition(RootBeanDefinition mbd, String beanName } @Override - @Nullable - protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { - return (this.preInstantiationPhase ? this.preInstantiationThread.get() != PreInstantiation.BACKGROUND : null); + protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { + 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 @@ -1047,8 +1096,8 @@ public void preInstantiateSingletons() throws BeansException { // 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); @@ -1061,8 +1110,8 @@ public void preInstantiateSingletons() throws BeansException { } } finally { + this.mainThreadPrefix = null; this.preInstantiationThread.remove(); - this.preInstantiationPhase = false; } if (!futures.isEmpty()) { @@ -1086,8 +1135,7 @@ public void preInstantiateSingletons() throws BeansException { } } - @Nullable - private CompletableFuture preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { + private @Nullable CompletableFuture preInstantiateSingleton(String beanName, RootBeanDefinition mbd) { if (mbd.isBackgroundInit()) { Executor executor = getBootstrapExecutor(); if (executor != null) { @@ -1156,6 +1204,12 @@ private void instantiateSingleton(String beanName) { } } + 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 @@ -1460,9 +1514,8 @@ public NamedBeanHolder resolveNamedBean(Class requiredType) throws Bea } @SuppressWarnings("unchecked") - @Nullable - private NamedBeanHolder resolveNamedBean( - ResolvableType requiredType, @Nullable Object[] args, boolean nonUniqueAsNull) throws BeansException { + private @Nullable NamedBeanHolder resolveNamedBean( + ResolvableType requiredType, @Nullable Object @Nullable [] args, boolean nonUniqueAsNull) throws BeansException { Assert.notNull(requiredType, "Required type must not be null"); String[] candidateNames = getBeanNamesForType(requiredType); @@ -1518,9 +1571,8 @@ else if (candidateNames.length > 1) { return null; } - @Nullable - private NamedBeanHolder resolveNamedBean( - String beanName, ResolvableType requiredType, @Nullable Object[] args) throws BeansException { + private @Nullable NamedBeanHolder resolveNamedBean( + String beanName, ResolvableType requiredType, @Nullable Object @Nullable [] args) throws BeansException { Object bean = getBean(beanName, null, args); if (bean instanceof NullBean) { @@ -1530,8 +1582,7 @@ private NamedBeanHolder resolveNamedBean( } @Override - @Nullable - public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, + public @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { descriptor.initParameterNameDiscovery(getParameterNameDiscoverer()); @@ -1555,9 +1606,8 @@ else if (descriptor.supportsLazyResolution()) { return doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter); } - @Nullable - @SuppressWarnings("NullAway") - public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public @Nullable Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException { InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor); @@ -1673,8 +1723,7 @@ public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable Str } } - @Nullable - private Object resolveInstance(Object candidate, DependencyDescriptor descriptor, Class type, String name) { + private @Nullable Object resolveInstance(Object candidate, DependencyDescriptor descriptor, Class type, String name) { Object result = candidate; if (result instanceof NullBean) { // Raise exception if null encountered for required injection point @@ -1687,11 +1736,9 @@ private Object resolveInstance(Object candidate, DependencyDescriptor descriptor throw new BeanNotOfRequiredTypeException(name, type, candidate.getClass()); } return result; - } - @Nullable - private Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, + private @Nullable Object resolveMultipleBeans(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { Class type = descriptor.getDependencyType(); @@ -1747,8 +1794,7 @@ else if (Map.class == type) { } - @Nullable - private Object resolveMultipleBeansFallback(DependencyDescriptor descriptor, @Nullable String beanName, + private @Nullable Object resolveMultipleBeansFallback(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { Class type = descriptor.getDependencyType(); @@ -1762,8 +1808,7 @@ else if (Map.class.isAssignableFrom(type) && type.isInterface()) { return null; } - @Nullable - private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName, + private @Nullable Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { Class elementType = descriptor.getResolvableType().asCollection().resolveGeneric(); @@ -1789,8 +1834,7 @@ private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @N return result; } - @Nullable - private Object resolveMultipleBeanMap(DependencyDescriptor descriptor, @Nullable String beanName, + private @Nullable Object resolveMultipleBeanMap(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { ResolvableType mapType = descriptor.getResolvableType().asMap(); @@ -1823,8 +1867,7 @@ private boolean isRequired(DependencyDescriptor descriptor) { return getAutowireCandidateResolver().isRequired(descriptor); } - @Nullable - private Comparator adaptDependencyComparator(Map matchingBeans) { + private @Nullable Comparator adaptDependencyComparator(Map matchingBeans) { Comparator comparator = getDependencyComparator(); if (comparator instanceof OrderComparator orderComparator) { return orderComparator.withSourceProvider( @@ -1924,7 +1967,7 @@ private void addCandidateEntry(Map candidates, String candidateN 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)); @@ -1939,8 +1982,7 @@ else if (containsSingleton(candidateName) || * @param descriptor the target dependency to match against * @return the name of the autowire candidate, or {@code null} if none found */ - @Nullable - protected String determineAutowireCandidate(Map candidates, DependencyDescriptor descriptor) { + protected @Nullable String determineAutowireCandidate(Map candidates, DependencyDescriptor descriptor) { Class requiredType = descriptor.getDependencyType(); // Step 1: check primary candidate String primaryCandidate = determinePrimaryCandidate(candidates, requiredType); @@ -1994,8 +2036,7 @@ protected String determineAutowireCandidate(Map candidates, Depe * @return the name of the primary candidate, or {@code null} if none found * @see #isPrimary(String, Object) */ - @Nullable - protected String determinePrimaryCandidate(Map candidates, Class requiredType) { + protected @Nullable String determinePrimaryCandidate(Map candidates, Class requiredType) { String primaryBeanName = null; // First pass: identify unique primary candidate for (Map.Entry entry : candidates.entrySet()) { @@ -2046,8 +2087,7 @@ else if (candidateLocal) { * the same highest priority value * @see #getPriority(Object) */ - @Nullable - protected String determineHighestPriorityCandidate(Map candidates, Class requiredType) { + protected @Nullable String determineHighestPriorityCandidate(Map candidates, Class requiredType) { String highestPriorityBeanName = null; Integer highestPriority = null; boolean highestPriorityConflictDetected = false; @@ -2127,8 +2167,7 @@ private boolean isFallback(String beanName) { * @param beanInstance the bean instance to check (can be {@code null}) * @return the priority assigned to that bean or {@code null} if none is set */ - @Nullable - protected Integer getPriority(Object beanInstance) { + protected @Nullable Integer getPriority(Object beanInstance) { Comparator comparator = getDependencyComparator(); if (comparator instanceof OrderComparator orderComparator) { return orderComparator.getPriority(beanInstance); @@ -2172,7 +2211,7 @@ protected boolean matchesBeanName(String beanName, @Nullable String candidateNam * 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) && @@ -2241,7 +2280,7 @@ private void checkBeanNotOfRequiredType(Class type, DependencyDescriptor desc * Create an {@link Optional} wrapper for the specified dependency. */ private Optional createOptionalDependency( - DependencyDescriptor descriptor, @Nullable String beanName, final Object... args) { + DependencyDescriptor descriptor, @Nullable String beanName, final @Nullable Object... args) { DependencyDescriptor descriptorToUse = new NestedDependencyDescriptor(descriptor) { @Override @@ -2388,8 +2427,7 @@ private class DependencyObjectProvider implements BeanObjectProvider { private final boolean optional; - @Nullable - private final String beanName; + private final @Nullable String beanName; public DependencyObjectProvider(DependencyDescriptor descriptor, @Nullable String beanName) { this.descriptor = new NestedDependencyDescriptor(descriptor); @@ -2412,7 +2450,7 @@ public Object getObject() throws BeansException { } @Override - public Object getObject(final Object... args) throws BeansException { + public Object getObject(final @Nullable Object... args) throws BeansException { if (this.optional) { return createOptionalDependency(this.descriptor, this.beanName, args); } @@ -2432,8 +2470,7 @@ public Object resolveCandidate(String beanName, Class requiredType, BeanFacto } @Override - @Nullable - public Object getIfAvailable() throws BeansException { + public @Nullable Object getIfAvailable() throws BeansException { try { if (this.optional) { return createOptionalDependency(this.descriptor, this.beanName); @@ -2472,8 +2509,7 @@ public void ifAvailable(Consumer dependencyConsumer) throws BeansExcepti } @Override - @Nullable - public Object getIfUnique() throws BeansException { + public @Nullable Object getIfUnique() throws BeansException { DependencyDescriptor descriptorToUse = new DependencyDescriptor(this.descriptor) { @Override public boolean isRequired() { @@ -2484,8 +2520,7 @@ public boolean usesStandardBeanLookup() { return true; } @Override - @Nullable - public Object resolveNotUnique(ResolvableType type, Map matchingBeans) { + public @Nullable Object resolveNotUnique(ResolvableType type, Map matchingBeans) { return null; } }; @@ -2516,8 +2551,7 @@ public void ifUnique(Consumer dependencyConsumer) throws BeansException } } - @Nullable - protected Object getValue() throws BeansException { + protected @Nullable Object getValue() throws BeansException { if (this.optional) { return createOptionalDependency(this.descriptor, this.beanName); } @@ -2544,8 +2578,8 @@ private Stream resolveStream(boolean ordered) { } @Override - public Stream stream(Predicate> customFilter) { - return Arrays.stream(getBeanNamesForTypedStream(this.descriptor.getResolvableType(), true)) + public Stream stream(Predicate> customFilter, boolean includeNonSingletons) { + return Arrays.stream(beanNamesForStream(this.descriptor.getResolvableType(), includeNonSingletons, true)) .filter(name -> AutowireUtils.isAutowireCandidate(DefaultListableBeanFactory.this, name)) .filter(name -> customFilter.test(getType(name))) .map(name -> getBean(name)) @@ -2553,8 +2587,8 @@ public Stream stream(Predicate> customFilter) { } @Override - public Stream orderedStream(Predicate> customFilter) { - String[] beanNames = getBeanNamesForTypedStream(this.descriptor.getResolvableType(), true); + public Stream orderedStream(Predicate> customFilter, boolean includeNonSingletons) { + String[] beanNames = beanNamesForStream(this.descriptor.getResolvableType(), includeNonSingletons, true); if (beanNames.length == 0) { return Stream.empty(); } @@ -2591,8 +2625,7 @@ public Jsr330Provider(DependencyDescriptor descriptor, @Nullable String beanName } @Override - @Nullable - public Object get() throws BeansException { + public @Nullable Object get() throws BeansException { return getValue(); } } @@ -2617,8 +2650,7 @@ public FactoryAwareOrderSourceProvider(Map instancesToBeanNames) } @Override - @Nullable - public Object getOrderSource(Object obj) { + public @Nullable Object getOrderSource(Object obj) { String beanName = this.instancesToBeanNames.get(obj); if (beanName == null) { return null; 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 fd88d2c44c72..b412589b6e9f 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; @@ -29,6 +30,8 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCreationNotAllowedException; import org.springframework.beans.factory.BeanCurrentlyInCreationException; @@ -36,7 +39,6 @@ import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.SimpleAliasRegistry; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -110,12 +112,17 @@ 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; /** Collection of suppressed Exceptions, available for associating related causes. */ - @Nullable - private Set suppressedExceptions; + private @Nullable Set suppressedExceptions; /** Disposable bean instances: bean name to disposable instance. */ private final Map disposableBeans = new LinkedHashMap<>(); @@ -186,8 +193,7 @@ public void addSingletonCallback(String beanName, Consumer singletonCons } @Override - @Nullable - public Object getSingleton(String beanName) { + public @Nullable Object getSingleton(String beanName) { return getSingleton(beanName, true); } @@ -199,8 +205,7 @@ public Object getSingleton(String beanName) { * @param allowEarlyReference whether early references should be created or not * @return the registered singleton object, or {@code null} if none found */ - @Nullable - protected Object getSingleton(String beanName, boolean allowEarlyReference) { + protected @Nullable Object getSingleton(String beanName, boolean allowEarlyReference) { // Quick check for existing instance without full singleton lock. Object singletonObject = this.singletonObjects.get(beanName); if (singletonObject == null && isSingletonCurrentlyInCreation(beanName)) { @@ -246,13 +251,15 @@ protected Object getSingleton(String beanName, boolean allowEarlyReference) { * with, if necessary * @return the registered singleton object */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation 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) { @@ -262,13 +269,15 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { // 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 { @@ -304,14 +313,27 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { 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); + } } } } @@ -344,7 +366,13 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { // 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; } } @@ -393,6 +421,8 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { this.lenientCreationLock.lock(); try { this.singletonsInLenientCreation.remove(beanName); + this.lenientWaitingThreads.entrySet().removeIf( + entry -> entry.getValue() == currentThread); this.lenientCreationFinished.signalAll(); } finally { @@ -401,18 +431,31 @@ public Object getSingleton(String beanName, ObjectFactory singletonFactory) { } } + 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 */ - @Nullable - protected Boolean isCurrentThreadAllowedToHoldSingletonLock() { + protected @Nullable Boolean isCurrentThreadAllowedToHoldSingletonLock() { return null; } @@ -707,12 +750,19 @@ public void destroySingleton(String beanName) { // 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/DisposableBeanAdapter.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java index f95fd951e3f2..1b0149e9febf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/DisposableBeanAdapter.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -35,7 +36,6 @@ import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -89,14 +89,11 @@ class DisposableBeanAdapter implements DisposableBean, Runnable, Serializable { private boolean invokeAutoCloseable; - @Nullable - private String[] destroyMethodNames; + private String @Nullable [] destroyMethodNames; - @Nullable - private transient Method[] destroyMethods; + private transient Method @Nullable [] destroyMethods; - @Nullable - private final List beanPostProcessors; + private final @Nullable List beanPostProcessors; /** @@ -177,7 +174,7 @@ public DisposableBeanAdapter(Object bean, List postProcessors) { this.bean = bean; @@ -261,8 +258,7 @@ else if (this.destroyMethodNames != null) { } - @Nullable - private Method determineDestroyMethod(String destroyMethodName) { + private @Nullable Method determineDestroyMethod(String destroyMethodName) { try { Class beanClass = this.bean.getClass(); MethodDescriptor descriptor = MethodDescriptor.create(this.beanName, beanClass, destroyMethodName); @@ -286,8 +282,7 @@ private Method determineDestroyMethod(String destroyMethodName) { } } - @Nullable - private Method findDestroyMethod(Class clazz, String name) { + private @Nullable Method findDestroyMethod(Class clazz, String name) { return (this.nonPublicAccessAllowed ? BeanUtils.findMethodWithMinimalParameters(clazz, name) : BeanUtils.findMethodWithMinimalParameters(clazz.getMethods(), name)); @@ -408,8 +403,7 @@ public static boolean hasDestroyMethod(Object bean, RootBeanDefinition beanDefin *

Also processes the {@link java.io.Closeable} and {@link java.lang.AutoCloseable} * interfaces, reflectively calling the "close" method on implementing beans as well. */ - @Nullable - static String[] inferDestroyMethodsIfNecessary(Class target, RootBeanDefinition beanDefinition) { + static String @Nullable [] inferDestroyMethodsIfNecessary(Class target, RootBeanDefinition beanDefinition) { String[] destroyMethodNames = beanDefinition.getDestroyMethodNames(); if (destroyMethodNames != null && destroyMethodNames.length > 1) { return destroyMethodNames; @@ -469,8 +463,7 @@ public static boolean hasApplicableProcessors(Object bean, List filterPostProcessors( + private static @Nullable List filterPostProcessors( List processors, Object bean) { List filteredPostProcessors = null; 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 ffcd87bbfbc1..dcec9ee5c120 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. @@ -19,6 +19,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanCurrentlyInCreationException; @@ -26,7 +28,6 @@ import org.springframework.beans.factory.FactoryBeanNotInitializedException; import org.springframework.core.AttributeAccessor; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * Support base class for singleton registries which need to handle @@ -50,8 +51,7 @@ public abstract class FactoryBeanRegistrySupport extends DefaultSingletonBeanReg * @return the FactoryBean's object type, * or {@code null} if the type cannot be determined yet */ - @Nullable - protected Class getTypeForFactoryBean(FactoryBean factoryBean) { + protected @Nullable Class getTypeForFactoryBean(FactoryBean factoryBean) { try { return factoryBean.getObjectType(); } @@ -102,8 +102,7 @@ ResolvableType getFactoryBeanGeneric(@Nullable ResolvableType type) { * @return the object obtained from the FactoryBean, * or {@code null} if not available */ - @Nullable - protected Object getCachedObjectForFactoryBean(String beanName) { + protected @Nullable Object getCachedObjectForFactoryBean(String beanName) { return this.factoryBeanObjectCache.get(beanName); } @@ -118,7 +117,15 @@ protected Object getCachedObjectForFactoryBean(String beanName) { */ 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) { @@ -131,11 +138,13 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam } 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); } @@ -144,7 +153,9 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam "Post-processing of FactoryBean's singleton object failed", ex); } finally { - afterSingletonCreation(beanName); + if (locked) { + afterSingletonCreation(beanName); + } } } if (containsSingleton(beanName)) { @@ -155,7 +166,9 @@ protected Object getObjectFromFactoryBean(FactoryBean factory, String beanNam 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/GenericBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java index 570957cf99a5..fdfb87480bad 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericBeanDefinition.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -40,10 +41,10 @@ * @see ChildBeanDefinition */ @SuppressWarnings("serial") -public class GenericBeanDefinition extends AbstractBeanDefinition { +public class +GenericBeanDefinition extends AbstractBeanDefinition { - @Nullable - private String parentName; + private @Nullable String parentName; /** @@ -74,8 +75,7 @@ public void setParentName(@Nullable String parentName) { } @Override - @Nullable - public String getParentName() { + public @Nullable String getParentName() { return this.parentName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java index f422594f701e..a7b537169898 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/GenericTypeAwareAutowireCandidateResolver.java @@ -19,6 +19,8 @@ import java.lang.reflect.Method; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; @@ -27,7 +29,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -45,8 +46,7 @@ public class GenericTypeAwareAutowireCandidateResolver extends SimpleAutowireCandidateResolver implements BeanFactoryAware, Cloneable { - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; @Override @@ -54,8 +54,7 @@ public void setBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - @Nullable - protected final BeanFactory getBeanFactory() { + protected final @Nullable BeanFactory getBeanFactory() { return this.beanFactory; } @@ -73,7 +72,7 @@ public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDesc * Match the given dependency type with its generic type information against the given * candidate bean definition. */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected boolean checkGenericTypeMatch(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) { ResolvableType dependencyType = descriptor.getResolvableType(); if (dependencyType.getType() instanceof Class) { @@ -161,8 +160,7 @@ else if (targetType.resolve() == Properties.class) { return dependencyType.isAssignableFrom(targetType); } - @Nullable - protected RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition rbd) { + protected @Nullable RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition rbd) { BeanDefinitionHolder decDef = rbd.getDecoratedDefinition(); if (decDef != null && this.beanFactory instanceof ConfigurableListableBeanFactory clbf) { if (clbf.containsBeanDefinition(decDef.getBeanName())) { @@ -175,8 +173,7 @@ protected RootBeanDefinition getResolvedDecoratedDefinition(RootBeanDefinition r return null; } - @Nullable - protected ResolvableType getReturnTypeForFactoryMethod(RootBeanDefinition rbd, DependencyDescriptor descriptor) { + protected @Nullable ResolvableType getReturnTypeForFactoryMethod(RootBeanDefinition rbd, DependencyDescriptor descriptor) { // Should typically be set for any kind of factory method, since the BeanFactory // pre-resolves them before reaching out to the AutowireCandidateResolver... ResolvableType returnType = rbd.factoryMethodReturnType; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java index 22e65bc12be5..76c68376fee5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstanceSupplier.java @@ -19,7 +19,8 @@ import java.lang.reflect.Method; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.function.ThrowingBiFunction; import org.springframework.util.function.ThrowingSupplier; @@ -59,8 +60,7 @@ default T getWithException() { * another means. * @return the factory method used to create the instance, or {@code null} */ - @Nullable - default Method getFactoryMethod() { + default @Nullable Method getFactoryMethod() { return null; } @@ -83,8 +83,7 @@ public V get(RegisteredBean registeredBean) throws Exception { return after.applyWithException(registeredBean, InstanceSupplier.this.get(registeredBean)); } @Override - @Nullable - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { return InstanceSupplier.this.getFactoryMethod(); } }; @@ -127,8 +126,7 @@ public T get(RegisteredBean registeredBean) throws Exception { return supplier.getWithException(); } @Override - @Nullable - public Method getFactoryMethod() { + public @Nullable Method getFactoryMethod() { return factoryMethod; } }; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java index 450d85aa9ec3..0824a2038b06 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/InstantiationStrategy.java @@ -19,9 +19,10 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; -import org.springframework.lang.Nullable; /** * Interface responsible for creating instances corresponding to a root bean definition. @@ -80,7 +81,7 @@ Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory * @throws BeansException if the instantiation attempt failed */ Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, Method factoryMethod, Object... args) + @Nullable Object factoryBean, Method factoryMethod, @Nullable Object... args) throws BeansException; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java index 9cbcbebe94e9..f0be69767c84 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/LookupOverride.java @@ -19,8 +19,9 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -41,11 +42,9 @@ */ public class LookupOverride extends MethodOverride { - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private Method method; + private @Nullable Method method; /** @@ -75,8 +74,7 @@ public LookupOverride(Method method, @Nullable String beanName) { /** * Return the name of the bean that should be returned by this {@code LookupOverride}. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java index 89e346b2d9b5..f0dead875813 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedArray.java @@ -16,7 +16,8 @@ package org.springframework.beans.factory.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -30,8 +31,7 @@ public class ManagedArray extends ManagedList { /** Resolved element type for runtime creation of the target array. */ - @Nullable - volatile Class resolvedElementType; + volatile @Nullable Class resolvedElementType; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java index 2b0a25a91396..43ad078b24f8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedList.java @@ -20,9 +20,10 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.Mergeable; -import org.springframework.lang.Nullable; /** * Tag collection class used to hold managed List elements, which may @@ -39,11 +40,9 @@ @SuppressWarnings("serial") public class ManagedList extends ArrayList implements Mergeable, BeanMetadataElement { - @Nullable - private Object source; + private @Nullable Object source; - @Nullable - private String elementTypeName; + private @Nullable String elementTypeName; private boolean mergeEnabled; @@ -80,8 +79,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -95,8 +93,7 @@ public void setElementTypeName(String elementTypeName) { /** * Return the default element type name (class name) to be used for this list. */ - @Nullable - public String getElementTypeName() { + public @Nullable String getElementTypeName() { return this.elementTypeName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java index b0eef75e3efa..f5af789f8acc 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedMap.java @@ -20,9 +20,10 @@ import java.util.Map; import java.util.Map.Entry; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.Mergeable; -import org.springframework.lang.Nullable; /** * Tag collection class used to hold managed Map values, which may @@ -37,14 +38,11 @@ @SuppressWarnings("serial") public class ManagedMap extends LinkedHashMap implements Mergeable, BeanMetadataElement { - @Nullable - private Object source; + private @Nullable Object source; - @Nullable - private String keyTypeName; + private @Nullable String keyTypeName; - @Nullable - private String valueTypeName; + private @Nullable String valueTypeName; private boolean mergeEnabled; @@ -86,8 +84,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -101,8 +98,7 @@ public void setKeyTypeName(@Nullable String keyTypeName) { /** * Return the default key type name (class name) to be used for this map. */ - @Nullable - public String getKeyTypeName() { + public @Nullable String getKeyTypeName() { return this.keyTypeName; } @@ -116,8 +112,7 @@ public void setValueTypeName(@Nullable String valueTypeName) { /** * Return the default value type name (class name) to be used for this map. */ - @Nullable - public String getValueTypeName() { + public @Nullable String getValueTypeName() { return this.valueTypeName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedProperties.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedProperties.java index ef00476dadee..951987f446c6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedProperties.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedProperties.java @@ -18,9 +18,10 @@ import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.Mergeable; -import org.springframework.lang.Nullable; /** * Tag class which represents a Spring-managed {@link Properties} instance @@ -33,8 +34,7 @@ @SuppressWarnings("serial") public class ManagedProperties extends Properties implements Mergeable, BeanMetadataElement { - @Nullable - private Object source; + private @Nullable Object source; private boolean mergeEnabled; @@ -48,8 +48,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java index 1381dde65152..dc25faf93d99 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ManagedSet.java @@ -20,9 +20,10 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; import org.springframework.beans.Mergeable; -import org.springframework.lang.Nullable; /** * Tag collection class used to hold managed Set values, which may @@ -38,11 +39,9 @@ @SuppressWarnings("serial") public class ManagedSet extends LinkedHashSet implements Mergeable, BeanMetadataElement { - @Nullable - private Object source; + private @Nullable Object source; - @Nullable - private String elementTypeName; + private @Nullable String elementTypeName; private boolean mergeEnabled; @@ -79,8 +78,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @@ -94,8 +92,7 @@ public void setElementTypeName(@Nullable String elementTypeName) { /** * Return the default element type name (class name) to be used for this set. */ - @Nullable - public String getElementTypeName() { + public @Nullable String getElementTypeName() { return this.elementTypeName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java index 49ca03408e5e..69d983e57021 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/MethodOverride.java @@ -19,8 +19,9 @@ import java.lang.reflect.Method; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanMetadataElement; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -42,8 +43,7 @@ public abstract class MethodOverride implements BeanMetadataElement { private boolean overloaded = true; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -90,8 +90,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } 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 d9d9e6c12177..5501fe22cafe 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. @@ -20,7 +20,7 @@ import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Set of method overrides, determining which, if any, methods on a @@ -87,11 +87,10 @@ public boolean isEmpty() { /** * 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 */ - @Nullable - public MethodOverride getOverride(Method method) { + public @Nullable MethodOverride getOverride(Method method) { MethodOverride match = null; for (MethodOverride candidate : this.overrides) { if (candidate.matches(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 e4e5df879e5b..e8d844db7d10 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/NullBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/NullBean.java index ab847865b5e0..51de5237ffe8 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/NullBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/NullBean.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; /** * Internal representation of a null bean instance, for example, for a {@code null} value 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 37f90946393a..66e122c384d7 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. @@ -25,6 +25,8 @@ import java.util.Properties; import java.util.ResourceBundle; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.PropertyAccessor; @@ -35,7 +37,6 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; import org.springframework.util.StringUtils; @@ -74,10 +75,10 @@ * @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 { /** @@ -145,8 +146,7 @@ public class PropertiesBeanDefinitionReader extends AbstractBeanDefinitionReader public static final String CONSTRUCTOR_ARG_PREFIX = "$"; - @Nullable - private String defaultParentBean; + private @Nullable String defaultParentBean; private PropertiesPersister propertiesPersister = DefaultPropertiesPersister.INSTANCE; @@ -180,8 +180,7 @@ public void setDefaultParentBean(@Nullable String defaultParentBean) { /** * Return the default parent bean for this bean factory. */ - @Nullable - public String getDefaultParentBean() { + public @Nullable String getDefaultParentBean() { return this.defaultParentBean; } @@ -405,10 +404,10 @@ public int registerBeanDefinitions(Map map, @Nullable String prefix, Strin /** * 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/factory/support/RegisteredBean.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java index 3f3b25cc5ab1..5bddb9b3887e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RegisteredBean.java @@ -23,6 +23,8 @@ import java.util.function.BiFunction; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.TypeConverter; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanDefinition; @@ -31,7 +33,6 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -57,8 +58,7 @@ public final class RegisteredBean { private final Supplier mergedBeanDefinition; - @Nullable - private final RegisteredBean parent; + private final @Nullable RegisteredBean parent; private RegisteredBean(ConfigurableListableBeanFactory beanFactory, Supplier beanName, @@ -202,8 +202,7 @@ public boolean isInnerBean() { * Return the parent of this instance or {@code null} if not an inner-bean. * @return the parent */ - @Nullable - public RegisteredBean getParent() { + public @Nullable RegisteredBean getParent() { return this.parent; } @@ -245,8 +244,7 @@ public InstantiationDescriptor resolveInstantiationDescriptor() { * @return the resolved object, or {@code null} if none found * @since 6.0.9 */ - @Nullable - public Object resolveAutowiredArgument( + public @Nullable Object resolveAutowiredArgument( DependencyDescriptor descriptor, TypeConverter typeConverter, Set autowiredBeanNames) { return new ConstructorResolver((AbstractAutowireCapableBeanFactory) getBeanFactory()) @@ -287,13 +285,11 @@ private static class InnerBeanResolver { private final RegisteredBean parent; - @Nullable - private final String innerBeanName; + private final @Nullable String innerBeanName; private final BeanDefinition innerBeanDefinition; - @Nullable - private volatile String resolvedBeanName; + private volatile @Nullable String resolvedBeanName; InnerBeanResolver(RegisteredBean parent, @Nullable String innerBeanName, BeanDefinition innerBeanDefinition) { Assert.isInstanceOf(AbstractAutowireCapableBeanFactory.class, parent.getBeanFactory()); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java index 4fe5ad846236..ffc3de60c00b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/ReplaceOverride.java @@ -21,7 +21,8 @@ import java.util.List; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java index 67aad00a312e..f0483f1c8e0f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/RootBeanDefinition.java @@ -26,12 +26,13 @@ import java.util.Set; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -62,11 +63,9 @@ @SuppressWarnings("serial") public class RootBeanDefinition extends AbstractBeanDefinition { - @Nullable - private BeanDefinitionHolder decoratedDefinition; + private @Nullable BeanDefinitionHolder decoratedDefinition; - @Nullable - private AnnotatedElement qualifiedElement; + private @Nullable AnnotatedElement qualifiedElement; /** Determines if the definition needs to be re-merged. */ volatile boolean stale; @@ -75,46 +74,37 @@ public class RootBeanDefinition extends AbstractBeanDefinition { boolean isFactoryMethodUnique; - @Nullable - volatile ResolvableType targetType; + volatile @Nullable ResolvableType targetType; /** Package-visible field for caching the determined Class of a given bean definition. */ - @Nullable - volatile Class resolvedTargetType; + volatile @Nullable Class resolvedTargetType; /** Package-visible field for caching if the bean is a factory bean. */ - @Nullable - volatile Boolean isFactoryBean; + volatile @Nullable Boolean isFactoryBean; /** Package-visible field for caching the return type of a generically typed factory method. */ - @Nullable - volatile ResolvableType factoryMethodReturnType; + volatile @Nullable ResolvableType factoryMethodReturnType; /** Package-visible field for caching a unique factory method candidate for introspection. */ - @Nullable - volatile Method factoryMethodToIntrospect; + volatile @Nullable Method factoryMethodToIntrospect; /** Package-visible field for caching a resolved destroy method name (also for inferred). */ - @Nullable - volatile String resolvedDestroyMethodName; + volatile @Nullable String resolvedDestroyMethodName; /** Common lock for the four constructor fields below. */ final Object constructorArgumentLock = new Object(); /** Package-visible field for caching the resolved constructor or factory method. */ - @Nullable - Executable resolvedConstructorOrFactoryMethod; + @Nullable Executable resolvedConstructorOrFactoryMethod; /** Package-visible field that marks the constructor arguments as resolved. */ boolean constructorArgumentsResolved = false; /** Package-visible field for caching fully resolved constructor arguments. */ - @Nullable - Object[] resolvedConstructorArguments; + @Nullable Object @Nullable [] resolvedConstructorArguments; /** Package-visible field for caching partly prepared constructor arguments. */ - @Nullable - Object[] preparedConstructorArguments; + @Nullable Object @Nullable [] preparedConstructorArguments; /** Common lock for the two post-processing fields below. */ final Object postProcessingLock = new Object(); @@ -123,17 +113,13 @@ public class RootBeanDefinition extends AbstractBeanDefinition { boolean postProcessed = false; /** Package-visible field that indicates a before-instantiation post-processor having kicked in. */ - @Nullable - volatile Boolean beforeInstantiationResolved; + volatile @Nullable Boolean beforeInstantiationResolved; - @Nullable - private Set externallyManagedConfigMembers; + private @Nullable Set externallyManagedConfigMembers; - @Nullable - private Set externallyManagedInitMethods; + private @Nullable Set externallyManagedInitMethods; - @Nullable - private Set externallyManagedDestroyMethods; + private @Nullable Set externallyManagedDestroyMethods; /** @@ -277,8 +263,7 @@ public RootBeanDefinition(RootBeanDefinition original) { @Override - @Nullable - public String getParentName() { + public @Nullable String getParentName() { return null; } @@ -299,8 +284,7 @@ public void setDecoratedDefinition(@Nullable BeanDefinitionHolder decoratedDefin /** * Return the target definition that is being decorated by this bean definition, if any. */ - @Nullable - public BeanDefinitionHolder getDecoratedDefinition() { + public @Nullable BeanDefinitionHolder getDecoratedDefinition() { return this.decoratedDefinition; } @@ -320,8 +304,7 @@ public void setQualifiedElement(@Nullable AnnotatedElement qualifiedElement) { * Otherwise, the factory method and target class will be checked. * @since 4.3.3 */ - @Nullable - public AnnotatedElement getQualifiedElement() { + public @Nullable AnnotatedElement getQualifiedElement() { return this.qualifiedElement; } @@ -346,8 +329,7 @@ public void setTargetType(@Nullable Class targetType) { * (either specified in advance or resolved on first instantiation). * @since 3.2.2 */ - @Nullable - public Class getTargetType() { + public @Nullable Class getTargetType() { if (this.resolvedTargetType != null) { return this.resolvedTargetType; } @@ -393,8 +375,7 @@ public ResolvableType getResolvableType() { * (in which case the regular no-arg default constructor will be called) * @since 5.1 */ - @Nullable - public Constructor[] getPreferredConstructors() { + public Constructor @Nullable [] getPreferredConstructors() { Object attribute = getAttribute(PREFERRED_CONSTRUCTORS_ATTRIBUTE); if (attribute == null) { return null; @@ -451,8 +432,7 @@ public void setResolvedFactoryMethod(@Nullable Method method) { * Return the resolved factory method as a Java Method object, if available. * @return the factory method, or {@code null} if not found or not resolved yet */ - @Nullable - public Method getResolvedFactoryMethod() { + public @Nullable Method getResolvedFactoryMethod() { Method factoryMethod = this.factoryMethodToIntrospect; if (factoryMethod == null && getInstanceSupplier() instanceof InstanceSupplier instanceSupplier) { @@ -508,8 +488,8 @@ public Set getExternallyManagedConfigMembers() { /** * Register an externally managed configuration initialization method — - * for example, a method annotated with JSR-250's {@code javax.annotation.PostConstruct} - * or Jakarta's {@link jakarta.annotation.PostConstruct} annotation. + * for example, a method annotated with Jakarta's + * {@link jakarta.annotation.PostConstruct} annotation. *

The supplied {@code initMethod} may be a * {@linkplain Method#getName() simple method name} or a * {@linkplain org.springframework.util.ClassUtils#getQualifiedMethodName(Method) diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java index 12041b3686da..a1f438195a0c 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleAutowireCandidateResolver.java @@ -66,10 +66,40 @@ public AutowireCandidateResolver cloneIfNecessary() { * @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate() * @see AbstractBeanDefinition#isDefaultCandidate() */ - @SuppressWarnings("unchecked") public static Map resolveAutowireCandidates(ConfigurableListableBeanFactory lbf, Class type) { + return resolveAutowireCandidates(lbf, type, true, true); + } + + /** + * Resolve a map of all beans of the given type, also picking up beans defined in + * ancestor bean factories, with the specific condition that each bean actually + * has autowire candidate status. This matches simple injection point resolution + * as implemented by this {@link AutowireCandidateResolver} strategy, including + * beans which are not marked as default candidates but excluding beans which + * are not even marked as autowire candidates. + * @param lbf the bean factory + * @param type the type of bean to match + * @param includeNonSingletons whether to include prototype or scoped beans too + * or just singletons (also applies to FactoryBeans) + * @param allowEagerInit whether to initialize lazy-init singletons and + * objects created by FactoryBeans (or by factory methods with a + * "factory-bean" reference) for the type check. Note that FactoryBeans need to be + * eagerly initialized to determine their type: So be aware that passing in "true" + * for this flag will initialize FactoryBeans and "factory-bean" references. + * @return the Map of matching bean instances, or an empty Map if none + * @throws BeansException if a bean could not be created + * @since 6.2.5 + * @see BeanFactoryUtils#beansOfTypeIncludingAncestors(ListableBeanFactory, Class, boolean, boolean) + * @see org.springframework.beans.factory.config.BeanDefinition#isAutowireCandidate() + * @see AbstractBeanDefinition#isDefaultCandidate() + */ + @SuppressWarnings("unchecked") + public static Map resolveAutowireCandidates(ConfigurableListableBeanFactory lbf, Class type, + boolean includeNonSingletons, boolean allowEagerInit) { + Map candidates = new LinkedHashMap<>(); - for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(lbf, type)) { + for (String beanName : BeanFactoryUtils.beanNamesForTypeIncludingAncestors(lbf, type, + includeNonSingletons, allowEagerInit)) { if (AutowireUtils.isAutowireCandidate(lbf, beanName)) { Object beanInstance = lbf.getBean(beanName); if (!(beanInstance instanceof NullBean)) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java index aec5fadd4ab8..51d427dc2091 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/SimpleInstantiationStrategy.java @@ -21,11 +21,12 @@ import java.lang.reflect.Method; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -50,28 +51,10 @@ public class SimpleInstantiationStrategy implements InstantiationStrategy { *

Allows factory method implementations to determine whether the current * caller is the container itself as opposed to user code. */ - @Nullable - public static Method getCurrentlyInvokedFactoryMethod() { + public static @Nullable Method getCurrentlyInvokedFactoryMethod() { return currentlyInvokedFactoryMethod.get(); } - /** - * Set the factory method currently being invoked or {@code null} to remove - * the current value, if any. - * @param method the factory method currently being invoked or {@code null} - * @since 6.0 - * @deprecated in favor of {@link #instantiateWithFactoryMethod(Method, Supplier)} - */ - @Deprecated(since = "6.2", forRemoval = true) - public static void setCurrentlyInvokedFactoryMethod(@Nullable Method method) { - if (method != null) { - currentlyInvokedFactoryMethod.set(method); - } - else { - currentlyInvokedFactoryMethod.remove(); - } - } - /** * Invoke the given {@code instanceSupplier} with the factory method exposed * as being invoked. @@ -163,7 +146,7 @@ protected Object instantiateWithMethodInjection(RootBeanDefinition bd, @Nullable @Override public Object instantiate(RootBeanDefinition bd, @Nullable String beanName, BeanFactory owner, - @Nullable Object factoryBean, Method factoryMethod, Object... args) { + @Nullable Object factoryBean, Method factoryMethod, @Nullable Object... args) { return instantiateWithFactoryMethod(factoryMethod, () -> { try { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java index c9bfc61de581..3fc5cc6ccbd6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/StaticListableBeanFactory.java @@ -26,6 +26,8 @@ import java.util.Set; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.BeanFactoryUtils; @@ -39,7 +41,6 @@ import org.springframework.beans.factory.SmartFactoryBean; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -154,7 +155,7 @@ public T getBean(String name, @Nullable Class requiredType) throws BeansE } @Override - public Object getBean(String name, Object... args) throws BeansException { + public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException { if (!ObjectUtils.isEmpty(args)) { throw new UnsupportedOperationException( "StaticListableBeanFactory does not support explicit bean creation arguments"); @@ -177,7 +178,7 @@ else if (beanNames.length > 1) { } @Override - public T getBean(Class requiredType, Object... args) throws BeansException { + public T getBean(Class requiredType, @Nullable Object @Nullable ... args) throws BeansException { if (!ObjectUtils.isEmpty(args)) { throw new UnsupportedOperationException( "StaticListableBeanFactory does not support explicit bean creation arguments"); @@ -231,14 +232,12 @@ public boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws N } @Override - @Nullable - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { return getType(name, true); } @Override - @Nullable - public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { String beanName = BeanFactoryUtils.transformedBeanName(name); Object bean = this.beans.get(beanName); @@ -302,7 +301,7 @@ else if (beanNames.length > 1) { } } @Override - public T getObject(Object... args) throws BeansException { + public T getObject(@Nullable Object... args) throws BeansException { String[] beanNames = getBeanNamesForType(requiredType); if (beanNames.length == 1) { return (T) getBean(beanNames[0], args); @@ -315,8 +314,7 @@ else if (beanNames.length > 1) { } } @Override - @Nullable - public T getIfAvailable() throws BeansException { + public @Nullable T getIfAvailable() throws BeansException { String[] beanNames = getBeanNamesForType(requiredType); if (beanNames.length == 1) { return (T) getBean(beanNames[0]); @@ -329,8 +327,7 @@ else if (beanNames.length > 1) { } } @Override - @Nullable - public T getIfUnique() throws BeansException { + public @Nullable T getIfUnique() throws BeansException { String[] beanNames = getBeanNamesForType(requiredType); if (beanNames.length == 1) { return (T) getBean(beanNames[0]); @@ -452,16 +449,14 @@ public Map getBeansWithAnnotation(Class an } @Override - @Nullable - public A findAnnotationOnBean(String beanName, Class annotationType) + public @Nullable A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException { return findAnnotationOnBean(beanName, annotationType, true); } @Override - @Nullable - public A findAnnotationOnBean( + public @Nullable A findAnnotationOnBean( String beanName, Class annotationType, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/support/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/support/package-info.java index 0a5599d3f0eb..f8bfd78b84df 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/support/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/support/package-info.java @@ -2,9 +2,7 @@ * Classes supporting the {@code org.springframework.beans.factory} package. * Contains abstract base classes for {@code BeanFactory} implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanConfigurerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanConfigurerSupport.java index 6584e16bb951..8d3b0261bc33 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanConfigurerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanConfigurerSupport.java @@ -18,6 +18,7 @@ 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.BeanCurrentlyInCreationException; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -52,11 +52,9 @@ public class BeanConfigurerSupport implements BeanFactoryAware, InitializingBean /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private volatile BeanWiringInfoResolver beanWiringInfoResolver; + private volatile @Nullable BeanWiringInfoResolver beanWiringInfoResolver; - @Nullable - private volatile ConfigurableListableBeanFactory beanFactory; + private volatile @Nullable ConfigurableListableBeanFactory beanFactory; /** @@ -92,8 +90,7 @@ public void setBeanFactory(BeanFactory beanFactory) { *

The default implementation builds a {@link ClassNameBeanWiringInfoResolver}. * @return the default BeanWiringInfoResolver (never {@code null}) */ - @Nullable - protected BeanWiringInfoResolver createDefaultBeanWiringInfoResolver() { + protected @Nullable BeanWiringInfoResolver createDefaultBeanWiringInfoResolver() { return new ClassNameBeanWiringInfoResolver(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfo.java b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfo.java index ac8e634cedbf..b7844ade0239 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfo.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfo.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.wiring; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.AutowireCapableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -49,8 +50,7 @@ public class BeanWiringInfo { public static final int AUTOWIRE_BY_TYPE = AutowireCapableBeanFactory.AUTOWIRE_BY_TYPE; - @Nullable - private String beanName; + private @Nullable String beanName; private boolean isDefaultBeanName = false; @@ -120,8 +120,7 @@ public boolean indicatesAutowiring() { /** * Return the specific bean name that this BeanWiringInfo points to, if any. */ - @Nullable - public String getBeanName() { + public @Nullable String getBeanName() { return this.beanName; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfoResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfoResolver.java index f6dc9bfcef49..74c3791b2707 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfoResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/BeanWiringInfoResolver.java @@ -16,7 +16,7 @@ package org.springframework.beans.factory.wiring; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface to be implemented by objects than can resolve bean name @@ -41,7 +41,6 @@ public interface BeanWiringInfoResolver { * @param beanInstance the bean instance to resolve info for * @return the BeanWiringInfo, or {@code null} if not found */ - @Nullable - BeanWiringInfo resolveWiringInfo(Object beanInstance); + @Nullable BeanWiringInfo resolveWiringInfo(Object beanInstance); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/package-info.java index c069d7d1af60..c251111e9236 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/wiring/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/wiring/package-info.java @@ -2,9 +2,7 @@ * Mechanism to determine bean wiring metadata from a bean instance. * Foundation for aspect-driven bean configuration. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.wiring; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java index 018c85123f9b..d73e52ba3697 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractBeanDefinitionParser.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,7 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.BeanDefinitionStoreException; @@ -25,7 +26,6 @@ import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -58,16 +58,16 @@ public abstract class AbstractBeanDefinitionParser implements BeanDefinitionPars @Override - @Nullable - public final BeanDefinition parse(Element element, ParserContext parserContext) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public final @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { AbstractBeanDefinition definition = parseInternal(element, parserContext); if (definition != null && !parserContext.isNested()) { try { String id = resolveId(element, definition, parserContext); if (!StringUtils.hasText(id)) { parserContext.getReaderContext().error( - "Id is required for element '" + parserContext.getDelegate().getLocalName(element) - + "' when used as a top-level tag", element); + "Id is required for element '" + parserContext.getDelegate().getLocalName(element) + + "' when used as a top-level tag", element); } String[] aliases = null; if (shouldParseNameAsAliases()) { @@ -150,8 +150,7 @@ protected void registerBeanDefinition(BeanDefinitionHolder definition, BeanDefin * @see #parse(org.w3c.dom.Element, ParserContext) * @see #postProcessComponentDefinition(org.springframework.beans.factory.parsing.BeanComponentDefinition) */ - @Nullable - protected abstract AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext); + protected abstract @Nullable AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext); /** * Should an ID be generated instead of read from the passed in {@link Element}? diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java index 75b70796e1cc..2bcba9af8efe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/AbstractSingleBeanDefinitionParser.java @@ -16,12 +16,12 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; -import org.springframework.lang.Nullable; /** * Base class for those {@link BeanDefinitionParser} implementations that @@ -98,8 +98,7 @@ protected final AbstractBeanDefinition parseInternal(Element element, ParserCont * @return the name of the parent bean for the currently parsed bean, * or {@code null} if none */ - @Nullable - protected String getParentName(Element element) { + protected @Nullable String getParentName(Element element) { return null; } @@ -115,8 +114,7 @@ protected String getParentName(Element element) { * the supplied {@code Element}, or {@code null} if none * @see #getBeanClassName */ - @Nullable - protected Class getBeanClass(Element element) { + protected @Nullable Class getBeanClass(Element element) { return null; } @@ -127,8 +125,7 @@ protected Class getBeanClass(Element element) { * the supplied {@code Element}, or {@code null} if none * @see #getBeanClass */ - @Nullable - protected String getBeanClassName(Element element) { + protected @Nullable String getBeanClassName(Element element) { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java index a92f282667e3..53da31cdc22f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParser.java @@ -16,10 +16,10 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.lang.Nullable; /** * Interface used by the {@link DefaultBeanDefinitionDocumentReader} to handle custom, @@ -52,7 +52,6 @@ public interface BeanDefinitionParser { * provides access to a {@link org.springframework.beans.factory.support.BeanDefinitionRegistry} * @return the primary {@link BeanDefinition} */ - @Nullable - BeanDefinition parse(Element element, ParserContext parserContext); + @Nullable BeanDefinition parse(Element element, ParserContext parserContext); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java index 84bf8629d5f4..be5b2b8a71f0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeanDefinitionParserDelegate.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; @@ -58,7 +59,6 @@ import org.springframework.beans.factory.support.ManagedSet; import org.springframework.beans.factory.support.MethodOverrides; import org.springframework.beans.factory.support.ReplaceOverride; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -262,8 +262,7 @@ public final XmlReaderContext getReaderContext() { * Invoke the {@link org.springframework.beans.factory.parsing.SourceExtractor} * to pull the source metadata from the supplied {@link Element}. */ - @Nullable - protected Object extractSource(Element ele) { + protected @Nullable Object extractSource(Element ele) { return this.readerContext.extractSource(ele); } @@ -388,8 +387,7 @@ public BeanDefinitionDefaults getBeanDefinitionDefaults() { * Return any patterns provided in the 'default-autowire-candidates' * attribute of the top-level {@code } element. */ - @Nullable - public String[] getAutowireCandidatePatterns() { + public String @Nullable [] getAutowireCandidatePatterns() { String candidatePattern = this.defaults.getAutowireCandidates(); return (candidatePattern != null ? StringUtils.commaDelimitedListToStringArray(candidatePattern) : null); } @@ -400,8 +398,7 @@ public String[] getAutowireCandidatePatterns() { * if there were errors during parse. Errors are reported to the * {@link org.springframework.beans.factory.parsing.ProblemReporter}. */ - @Nullable - public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { + public @Nullable BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { return parseBeanDefinitionElement(ele, null); } @@ -410,9 +407,7 @@ public BeanDefinitionHolder parseBeanDefinitionElement(Element ele) { * if there were errors during parse. Errors are reported to the * {@link org.springframework.beans.factory.parsing.ProblemReporter}. */ - @Nullable - @SuppressWarnings("NullAway") - public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { + public @Nullable BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable BeanDefinition containingBean) { String id = ele.getAttribute(ID_ATTRIBUTE); String nameAttr = ele.getAttribute(NAME_ATTRIBUTE); @@ -461,7 +456,8 @@ public BeanDefinitionHolder parseBeanDefinitionElement(Element ele, @Nullable Be } } catch (Exception ex) { - error(ex.getMessage(), ele); + String message = ex.getMessage(); + error(message == null ? "" : message, ele); return null; } } @@ -497,8 +493,7 @@ protected void checkNameUniqueness(String beanName, List aliases, Elemen * Parse the bean definition itself, without regard to name or aliases. May return * {@code null} if problems occurred during the parsing of the bean definition. */ - @Nullable - public AbstractBeanDefinition parseBeanDefinitionElement( + public @Nullable AbstractBeanDefinition parseBeanDefinitionElement( Element ele, String beanName, @Nullable BeanDefinition containingBean) { this.parseState.push(new BeanEntry(beanName)); @@ -906,8 +901,7 @@ public void parseQualifierElement(Element ele, AbstractBeanDefinition bd) { * Get the value of a property element. May be a list etc. * Also used for constructor arguments, "propertyName" being null in this case. */ - @Nullable - public Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { + public @Nullable Object parsePropertyValue(Element ele, BeanDefinition bd, @Nullable String propertyName) { String elementName = (propertyName != null ? " element for property '" + propertyName + "'" : " element"); @@ -967,8 +961,7 @@ else if (subElement != null) { * @param ele subelement of property element; we don't know which yet * @param bd the current bean definition (if any) */ - @Nullable - public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { + public @Nullable Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) { return parsePropertySubElement(ele, bd, null); } @@ -980,8 +973,7 @@ public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd) * @param defaultValueType the default type (class name) for any * {@code } tag that might be created */ - @Nullable - public Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { + public @Nullable Object parsePropertySubElement(Element ele, @Nullable BeanDefinition bd, @Nullable String defaultValueType) { if (!isDefaultNamespace(ele)) { return parseNestedCustomElement(ele, bd); } @@ -1050,8 +1042,7 @@ else if (nodeNameEquals(ele, PROPS_ELEMENT)) { /** * Return a typed String value Object for the given 'idref' element. */ - @Nullable - public Object parseIdRefElement(Element ele) { + public @Nullable Object parseIdRefElement(Element ele) { // A generic reference to any name of any bean. String refName = ele.getAttribute(BEAN_REF_ATTRIBUTE); if (!StringUtils.hasLength(refName)) { @@ -1304,8 +1295,7 @@ protected final Object buildTypedStringValueForMap(String value, String defaultT /** * Parse a key sub-element of a map element. */ - @Nullable - protected Object parseKeyElement(Element keyEle, @Nullable BeanDefinition bd, String defaultKeyTypeName) { + protected @Nullable Object parseKeyElement(Element keyEle, @Nullable BeanDefinition bd, String defaultKeyTypeName) { NodeList nl = keyEle.getChildNodes(); Element subElement = null; for (int i = 0; i < nl.getLength(); i++) { @@ -1366,8 +1356,7 @@ public boolean parseMergeAttribute(Element collectionElement) { * @param ele the element to parse * @return the resulting bean definition */ - @Nullable - public BeanDefinition parseCustomElement(Element ele) { + public @Nullable BeanDefinition parseCustomElement(Element ele) { return parseCustomElement(ele, null); } @@ -1377,8 +1366,7 @@ public BeanDefinition parseCustomElement(Element ele) { * @param containingBd the containing bean definition (if any) * @return the resulting bean definition */ - @Nullable - public BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + public @Nullable BeanDefinition parseCustomElement(Element ele, @Nullable BeanDefinition containingBd) { String namespaceUri = getNamespaceURI(ele); if (namespaceUri == null) { return null; @@ -1465,8 +1453,7 @@ else if (namespaceUri.startsWith("http://www.springframework.org/schema/")) { return originalDef; } - @Nullable - private BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable BeanDefinition containingBd) { + private @Nullable BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable BeanDefinition containingBd) { BeanDefinition innerDefinition = parseCustomElement(ele, containingBd); if (innerDefinition == null) { error("Incorrect usage of element '" + ele.getNodeName() + "' in a nested manner. " + @@ -1490,8 +1477,7 @@ private BeanDefinitionHolder parseNestedCustomElement(Element ele, @Nullable Bea * different namespace identification mechanism. * @param node the node */ - @Nullable - public String getNamespaceURI(Node node) { + public @Nullable String getNamespaceURI(Node node) { return node.getNamespaceURI(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java index 16496d31b9b4..053b165fed3b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/BeansDtdResolver.java @@ -21,12 +21,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * {@link EntityResolver} implementation for the Spring beans DTD, @@ -52,8 +52,7 @@ public class BeansDtdResolver implements EntityResolver { @Override - @Nullable - public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { + public @Nullable InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { if (logger.isTraceEnabled()) { logger.trace("Trying to resolve XML entity with public ID [" + publicId + "] and system ID [" + systemId + "]"); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java index b75e54893a74..4c2a6e6c2b5b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultBeanDefinitionDocumentReader.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -34,7 +35,6 @@ import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourcePatternUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -77,11 +77,9 @@ public class DefaultBeanDefinitionDocumentReader implements BeanDefinitionDocume protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private XmlReaderContext readerContext; + private @Nullable XmlReaderContext readerContext; - @Nullable - private BeanDefinitionParserDelegate delegate; + private @Nullable BeanDefinitionParserDelegate delegate; /** @@ -108,8 +106,7 @@ protected final XmlReaderContext getReaderContext() { * Invoke the {@link org.springframework.beans.factory.parsing.SourceExtractor} * to pull the source metadata from the supplied {@link Element}. */ - @Nullable - protected Object extractSource(Element ele) { + protected @Nullable Object extractSource(Element ele) { return getReaderContext().extractSource(ele); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java index 77ec6469f13e..2a359b740176 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultDocumentLoader.java @@ -22,12 +22,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; import org.xml.sax.InputSource; -import org.springframework.lang.Nullable; import org.springframework.util.xml.XmlValidationModeDetector; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java index 68a96ee9d295..cfa04a15e661 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DefaultNamespaceHandlerResolver.java @@ -23,11 +23,11 @@ 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.FatalBeanException; import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -59,15 +59,13 @@ public class DefaultNamespaceHandlerResolver implements NamespaceHandlerResolver protected final Log logger = LogFactory.getLog(getClass()); /** ClassLoader to use for NamespaceHandler classes. */ - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; /** Resource location to search for. */ private final String handlerMappingsLocation; /** Stores the mappings from namespace URI to NamespaceHandler class name / instance. */ - @Nullable - private volatile Map handlerMappings; + private volatile @Nullable Map handlerMappings; /** @@ -113,8 +111,7 @@ public DefaultNamespaceHandlerResolver(@Nullable ClassLoader classLoader, String * @return the located {@link NamespaceHandler}, or {@code null} if none found */ @Override - @Nullable - public NamespaceHandler resolve(String namespaceUri) { + public @Nullable NamespaceHandler resolve(String namespaceUri) { Map handlerMappings = getHandlerMappings(); Object handlerOrClassName = handlerMappings.get(namespaceUri); if (handlerOrClassName == null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java index fe8f6f61a37f..1edf6ea01804 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DelegatingEntityResolver.java @@ -18,11 +18,11 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.xml.sax.SAXException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -78,8 +78,7 @@ public DelegatingEntityResolver(EntityResolver dtdResolver, EntityResolver schem @Override - @Nullable - public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) + public @Nullable InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws SAXException, IOException { if (systemId != null) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java index d5a2122a61e9..afb1968e0fad 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/DocumentDefaultsDefinition.java @@ -16,8 +16,9 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.parsing.DefaultsDefinition; -import org.springframework.lang.Nullable; /** * Simple JavaBean that holds the defaults specified at the {@code } @@ -29,26 +30,19 @@ */ public class DocumentDefaultsDefinition implements DefaultsDefinition { - @Nullable - private String lazyInit; + private @Nullable String lazyInit; - @Nullable - private String merge; + private @Nullable String merge; - @Nullable - private String autowire; + private @Nullable String autowire; - @Nullable - private String autowireCandidates; + private @Nullable String autowireCandidates; - @Nullable - private String initMethod; + private @Nullable String initMethod; - @Nullable - private String destroyMethod; + private @Nullable String destroyMethod; - @Nullable - private Object source; + private @Nullable Object source; /** @@ -61,8 +55,7 @@ public void setLazyInit(@Nullable String lazyInit) { /** * Return the default lazy-init flag for the document that's currently parsed. */ - @Nullable - public String getLazyInit() { + public @Nullable String getLazyInit() { return this.lazyInit; } @@ -76,8 +69,7 @@ public void setMerge(@Nullable String merge) { /** * Return the default merge setting for the document that's currently parsed. */ - @Nullable - public String getMerge() { + public @Nullable String getMerge() { return this.merge; } @@ -91,8 +83,7 @@ public void setAutowire(@Nullable String autowire) { /** * Return the default autowire setting for the document that's currently parsed. */ - @Nullable - public String getAutowire() { + public @Nullable String getAutowire() { return this.autowire; } @@ -108,8 +99,7 @@ public void setAutowireCandidates(@Nullable String autowireCandidates) { * Return the default autowire-candidate pattern for the document that's currently parsed. * May also return a comma-separated list of patterns. */ - @Nullable - public String getAutowireCandidates() { + public @Nullable String getAutowireCandidates() { return this.autowireCandidates; } @@ -123,8 +113,7 @@ public void setInitMethod(@Nullable String initMethod) { /** * Return the default init-method setting for the document that's currently parsed. */ - @Nullable - public String getInitMethod() { + public @Nullable String getInitMethod() { return this.initMethod; } @@ -138,8 +127,7 @@ public void setDestroyMethod(@Nullable String destroyMethod) { /** * Return the default destroy-method setting for the document that's currently parsed. */ - @Nullable - public String getDestroyMethod() { + public @Nullable String getDestroyMethod() { return this.destroyMethod; } @@ -152,8 +140,7 @@ public void setSource(@Nullable Object source) { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java index fa061fe0c181..2dee6de1a66a 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandler.java @@ -16,12 +16,12 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.lang.Nullable; /** * Base interface used by the {@link DefaultBeanDefinitionDocumentReader} @@ -69,8 +69,7 @@ public interface NamespaceHandler { * @param parserContext the object encapsulating the current state of the parsing process * @return the primary {@code BeanDefinition} (can be {@code null} as explained above) */ - @Nullable - BeanDefinition parse(Element element, ParserContext parserContext); + @Nullable BeanDefinition parse(Element element, ParserContext parserContext); /** * Parse the specified {@link Node} and decorate the supplied @@ -91,7 +90,6 @@ public interface NamespaceHandler { * A {@code null} value is strictly speaking invalid, but will be leniently * treated like the case where the original bean definition gets returned. */ - @Nullable - BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext); + @Nullable BeanDefinitionHolder decorate(Node source, BeanDefinitionHolder definition, ParserContext parserContext); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java index 2e92b258cac2..6707478efe4e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerResolver.java @@ -16,7 +16,7 @@ package org.springframework.beans.factory.xml; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Used by the {@link org.springframework.beans.factory.xml.DefaultBeanDefinitionDocumentReader} to @@ -36,7 +36,6 @@ public interface NamespaceHandlerResolver { * @param namespaceUri the relevant namespace URI * @return the located {@link NamespaceHandler} (may be {@code null}) */ - @Nullable - NamespaceHandler resolve(String namespaceUri); + @Nullable NamespaceHandler resolve(String namespaceUri); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java index b1eec9bbc9f9..3715f1181568 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/NamespaceHandlerSupport.java @@ -19,13 +19,13 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; -import org.springframework.lang.Nullable; /** * Support class for implementing custom {@link NamespaceHandler NamespaceHandlers}. @@ -68,8 +68,7 @@ public abstract class NamespaceHandlerSupport implements NamespaceHandler { * registered for that {@link Element}. */ @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { BeanDefinitionParser parser = findParserForElement(element, parserContext); return (parser != null ? parser.parse(element, parserContext) : null); } @@ -78,8 +77,7 @@ public BeanDefinition parse(Element element, ParserContext parserContext) { * Locates the {@link BeanDefinitionParser} from the register implementations using * the local name of the supplied {@link Element}. */ - @Nullable - private BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { + private @Nullable BeanDefinitionParser findParserForElement(Element element, ParserContext parserContext) { String localName = parserContext.getDelegate().getLocalName(element); BeanDefinitionParser parser = this.parsers.get(localName); if (parser == null) { @@ -94,8 +92,7 @@ private BeanDefinitionParser findParserForElement(Element element, ParserContext * is registered to handle that {@link Node}. */ @Override - @Nullable - public BeanDefinitionHolder decorate( + public @Nullable BeanDefinitionHolder decorate( Node node, BeanDefinitionHolder definition, ParserContext parserContext) { BeanDefinitionDecorator decorator = findDecoratorForNode(node, parserContext); @@ -107,8 +104,7 @@ public BeanDefinitionHolder decorate( * the local name of the supplied {@link Node}. Supports both {@link Element Elements} * and {@link Attr Attrs}. */ - @Nullable - private BeanDefinitionDecorator findDecoratorForNode(Node node, ParserContext parserContext) { + private @Nullable BeanDefinitionDecorator findDecoratorForNode(Node node, ParserContext parserContext) { BeanDefinitionDecorator decorator = null; String localName = parserContext.getDelegate().getLocalName(node); if (node instanceof Element) { diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java index 4bd6ef58e966..db4bad7251fd 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ParserContext.java @@ -19,13 +19,14 @@ import java.util.ArrayDeque; import java.util.Deque; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.parsing.BeanComponentDefinition; import org.springframework.beans.factory.parsing.ComponentDefinition; import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.lang.Nullable; /** * Context that gets passed along a bean definition parsing process, @@ -44,8 +45,7 @@ public final class ParserContext { private final BeanDefinitionParserDelegate delegate; - @Nullable - private BeanDefinition containingBeanDefinition; + private @Nullable BeanDefinition containingBeanDefinition; private final Deque containingComponents = new ArrayDeque<>(); @@ -76,8 +76,7 @@ public BeanDefinitionParserDelegate getDelegate() { return this.delegate; } - @Nullable - public BeanDefinition getContainingBeanDefinition() { + public @Nullable BeanDefinition getContainingBeanDefinition() { return this.containingBeanDefinition; } @@ -89,13 +88,11 @@ public boolean isDefaultLazyInit() { return BeanDefinitionParserDelegate.TRUE_VALUE.equals(this.delegate.getDefaults().getLazyInit()); } - @Nullable - public Object extractSource(Object sourceCandidate) { + public @Nullable Object extractSource(Object sourceCandidate) { return this.readerContext.extractSource(sourceCandidate); } - @Nullable - public CompositeComponentDefinition getContainingComponent() { + public @Nullable CompositeComponentDefinition getContainingComponent() { return this.containingComponents.peek(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java index 659b21b40b97..bbf56e67b1a0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/PluggableSchemaResolver.java @@ -24,13 +24,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -66,14 +66,12 @@ public class PluggableSchemaResolver implements EntityResolver { private static final Log logger = LogFactory.getLog(PluggableSchemaResolver.class); - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final String schemaMappingsLocation; /** Stores the mapping of schema URL → local schema path. */ - @Nullable - private volatile Map schemaMappings; + private volatile @Nullable Map schemaMappings; /** @@ -105,8 +103,7 @@ public PluggableSchemaResolver(@Nullable ClassLoader classLoader, String schemaM @Override - @Nullable - public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { + public @Nullable InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws IOException { if (logger.isTraceEnabled()) { logger.trace("Trying to resolve XML entity with public id [" + publicId + "] and system id [" + systemId + "]"); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java index 1b348693c9b7..512f09af5e39 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/ResourceEntityResolver.java @@ -23,12 +23,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.xml.sax.InputSource; import org.xml.sax.SAXException; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.ResourceUtils; /** @@ -72,8 +72,7 @@ public ResourceEntityResolver(ResourceLoader resourceLoader) { @Override - @Nullable - public InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) + public @Nullable InputSource resolveEntity(@Nullable String publicId, @Nullable String systemId) throws SAXException, IOException { InputSource source = super.resolveEntity(publicId, systemId); @@ -135,8 +134,7 @@ else if (systemId.endsWith(DTD_SUFFIX) || systemId.endsWith(XSD_SUFFIX)) { * that the parser open a regular URI connection to the system identifier * @since 6.0.4 */ - @Nullable - protected InputSource resolveSchemaEntity(@Nullable String publicId, String systemId) { + protected @Nullable InputSource resolveSchemaEntity(@Nullable String publicId, String systemId) { InputSource source; // External dtd/xsd lookup via https even for canonical http declaration String url = systemId; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java index 7cf160d848f0..ab956dd83f6e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimpleConstructorNamespaceHandler.java @@ -18,6 +18,7 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -28,7 +29,6 @@ import org.springframework.beans.factory.config.ConstructorArgumentValues.ValueHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.Conventions; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -69,8 +69,7 @@ public void init() { } @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { parserContext.getReaderContext().error( "Class [" + getClass().getName() + "] does not support custom elements.", element); return null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java index ec3c1512d8a8..9ea98fb50045 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/SimplePropertyNamespaceHandler.java @@ -16,6 +16,7 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Attr; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -25,7 +26,6 @@ import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.core.Conventions; -import org.springframework.lang.Nullable; /** * Simple {@code NamespaceHandler} implementation that maps custom attributes @@ -58,8 +58,7 @@ public void init() { } @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { parserContext.getReaderContext().error( "Class [" + getClass().getName() + "] does not support custom elements.", element); return null; diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java index 12232bddf71c..e231df3479fa 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlBeanDefinitionReader.java @@ -24,6 +24,7 @@ import javax.xml.parsers.ParserConfigurationException; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.xml.sax.EntityResolver; import org.xml.sax.ErrorHandler; @@ -46,7 +47,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.xml.SimpleSaxErrorHandler; import org.springframework.util.xml.XmlValidationModeDetector; @@ -124,13 +124,11 @@ public class XmlBeanDefinitionReader extends AbstractBeanDefinitionReader { private SourceExtractor sourceExtractor = new NullSourceExtractor(); - @Nullable - private NamespaceHandlerResolver namespaceHandlerResolver; + private @Nullable NamespaceHandlerResolver namespaceHandlerResolver; private DocumentLoader documentLoader = new DefaultDocumentLoader(); - @Nullable - private EntityResolver entityResolver; + private @Nullable EntityResolver entityResolver; private ErrorHandler errorHandler = new SimpleSaxErrorHandler(logger); diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java index a0ca6d0c2045..772a96c6e566 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/XmlReaderContext.java @@ -18,6 +18,7 @@ import java.io.StringReader; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.xml.sax.InputSource; @@ -31,7 +32,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; /** * Extension of {@link org.springframework.beans.factory.parsing.ReaderContext}, @@ -91,8 +91,7 @@ public final BeanDefinitionRegistry getRegistry() { * @see XmlBeanDefinitionReader#setResourceLoader * @see ResourceLoader#getClassLoader() */ - @Nullable - public final ResourceLoader getResourceLoader() { + public final @Nullable ResourceLoader getResourceLoader() { return this.reader.getResourceLoader(); } @@ -102,8 +101,7 @@ public final ResourceLoader getResourceLoader() { * as an indication to lazily resolve bean classes. * @see XmlBeanDefinitionReader#setBeanClassLoader */ - @Nullable - public final ClassLoader getBeanClassLoader() { + public final @Nullable ClassLoader getBeanClassLoader() { return this.reader.getBeanClassLoader(); } diff --git a/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java b/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java index 3dcc0d43ad0b..8c4648abb0ac 100644 --- a/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/factory/xml/package-info.java @@ -2,9 +2,7 @@ * Contains an abstract XML-based {@code BeanFactory} implementation, * including a standard "spring-beans" XSD. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.factory.xml; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/package-info.java b/spring-beans/src/main/java/org/springframework/beans/package-info.java index 1bea8aea4582..2cd047cb6588 100644 --- a/spring-beans/src/main/java/org/springframework/beans/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/package-info.java @@ -9,9 +9,7 @@ * Expert One-On-One J2EE Design and Development * by Rod Johnson (Wrox, 2002). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java index 14e4c4b80966..d2da2bd97227 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ByteArrayPropertyEditor.java @@ -18,7 +18,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Editor for byte arrays. Strings will simply be converted to diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java index 705d58fadfab..15de6ae48908 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CharArrayPropertyEditor.java @@ -18,7 +18,7 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Editor for char arrays. Strings will simply be converted to 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 727be75c8694..873c69465733 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,8 +17,10 @@ package org.springframework.beans.propertyeditors; import java.beans.PropertyEditorSupport; +import java.util.HexFormat; + +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -96,13 +98,12 @@ public String getAsText() { 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/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java index 0a2882a988c0..c8da0425ac30 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassArrayEditor.java @@ -19,7 +19,8 @@ import java.beans.PropertyEditorSupport; import java.util.StringJoiner; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -38,8 +39,7 @@ */ public class ClassArrayEditor extends PropertyEditorSupport { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java index a68d4988e49d..5176ea58e031 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ClassEditor.java @@ -18,7 +18,8 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -38,8 +39,7 @@ */ public class ClassEditor extends PropertyEditorSupport { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java index 5d71fca9daee..ddc9703cfcc2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomBooleanEditor.java @@ -18,7 +18,8 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -79,11 +80,9 @@ public class CustomBooleanEditor extends PropertyEditorSupport { public static final String VALUE_0 = "0"; - @Nullable - private final String trueString; + private final @Nullable String trueString; - @Nullable - private final String falseString; + private final @Nullable String falseString; private final boolean allowEmpty; diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java index 898adb52ecca..ce97ddd02197 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomCollectionEditor.java @@ -25,7 +25,8 @@ import java.util.SortedSet; import java.util.TreeSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -206,8 +207,7 @@ protected Object convertElement(Object element) { * there is no appropriate text representation. */ @Override - @Nullable - public String getAsText() { + public @Nullable String getAsText() { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java index fcc3f8290a2b..34d9d475fcc5 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomDateEditor.java @@ -21,7 +21,8 @@ import java.text.ParseException; import java.util.Date; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java index d421a8e25c00..b8dddef9a3bf 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomMapEditor.java @@ -22,7 +22,8 @@ import java.util.SortedMap; import java.util.TreeMap; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -196,8 +197,7 @@ protected Object convertValue(Object value) { * there is no appropriate text representation. */ @Override - @Nullable - public String getAsText() { + public @Nullable String getAsText() { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java index e1c8ba38376f..fb36494eedc6 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/CustomNumberEditor.java @@ -19,7 +19,8 @@ import java.beans.PropertyEditorSupport; import java.text.NumberFormat; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; @@ -47,8 +48,7 @@ public class CustomNumberEditor extends PropertyEditorSupport { private final Class numberClass; - @Nullable - private final NumberFormat numberFormat; + private final @Nullable NumberFormat numberFormat; private final boolean allowEmpty; diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java index 303067c5e253..bdf47645ffd1 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/InputStreamEditor.java @@ -19,9 +19,10 @@ import java.beans.PropertyEditorSupport; import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,8 +82,7 @@ public void setAsText(String text) throws IllegalArgumentException { * there is no appropriate text representation. */ @Override - @Nullable - public String getAsText() { + public @Nullable String getAsText() { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java index 03f14d117ede..da64aa724af2 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PatternEditor.java @@ -19,7 +19,7 @@ import java.beans.PropertyEditorSupport; import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Editor for {@code java.util.regex.Pattern}, to directly populate a Pattern property. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java index cccb6c6bfa4d..128b91c73948 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/PropertiesEditor.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.Properties; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Custom {@link java.beans.PropertyEditor} for {@link Properties} objects. diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java index 46171fb4b774..676b129ce61b 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/ReaderEditor.java @@ -19,10 +19,11 @@ import java.beans.PropertyEditorSupport; import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,8 +82,7 @@ public void setAsText(String text) throws IllegalArgumentException { * there is no appropriate text representation. */ @Override - @Nullable - public String getAsText() { + public @Nullable String getAsText() { return null; } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java index 7149e931df03..d99b9c277f5f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringArrayPropertyEditor.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. @@ -18,7 +18,8 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -44,8 +45,7 @@ public class StringArrayPropertyEditor extends PropertyEditorSupport { private final String separator; - @Nullable - private final String charsToDelete; + private final @Nullable String charsToDelete; private final boolean emptyArrayAsNull; @@ -127,7 +127,7 @@ public StringArrayPropertyEditor( @Override public void setAsText(String text) throws IllegalArgumentException { - String[] array = StringUtils.delimitedListToStringArray(text, this.separator, this.charsToDelete); + @Nullable String[] array = StringUtils.delimitedListToStringArray(text, this.separator, this.charsToDelete); if (this.emptyArrayAsNull && array.length == 0) { setValue(null); } diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java index d97037c56364..a87b898d34d0 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/StringTrimmerEditor.java @@ -18,7 +18,8 @@ import java.beans.PropertyEditorSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -32,8 +33,7 @@ */ public class StringTrimmerEditor extends PropertyEditorSupport { - @Nullable - private final String charsToDelete; + private final @Nullable String charsToDelete; private final boolean emptyAsNull; diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java index e94e65f5a94f..7a7afd224637 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/URIEditor.java @@ -21,8 +21,9 @@ import java.net.URI; import java.net.URISyntaxException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.ClassPathResource; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -50,8 +51,7 @@ */ public class URIEditor extends PropertyEditorSupport { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final boolean encode; diff --git a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java index ddb64ffdc167..e1dfba14bc7f 100644 --- a/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/propertyeditors/package-info.java @@ -6,9 +6,7 @@ * "CustomXxxEditor" classes are intended for manual registration in * specific binding processes, as they are localized or the like. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.propertyeditors; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java b/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java index 20dec0c3559b..876b7447142e 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/ArgumentConvertingMethodInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,12 @@ import java.beans.PropertyEditor; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyEditorRegistry; import org.springframework.beans.SimpleTypeConverter; import org.springframework.beans.TypeConverter; import org.springframework.beans.TypeMismatchException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MethodInvoker; import org.springframework.util.ReflectionUtils; @@ -41,8 +42,7 @@ */ public class ArgumentConvertingMethodInvoker extends MethodInvoker { - @Nullable - private TypeConverter typeConverter; + private @Nullable TypeConverter typeConverter; private boolean useDefaultConverter = true; @@ -67,8 +67,7 @@ public void setTypeConverter(@Nullable TypeConverter typeConverter) { * (provided that the present TypeConverter actually implements the * PropertyEditorRegistry interface). */ - @Nullable - public TypeConverter getTypeConverter() { + public @Nullable TypeConverter getTypeConverter() { if (this.typeConverter == null && this.useDefaultConverter) { this.typeConverter = getDefaultTypeConverter(); } @@ -111,8 +110,7 @@ public void registerCustomEditor(Class requiredType, PropertyEditor propertyE * @see #doFindMatchingMethod */ @Override - @Nullable - protected Method findMatchingMethod() { + protected @Nullable Method findMatchingMethod() { Method matchingMethod = super.findMatchingMethod(); // Second pass: look for method where arguments can be converted to parameter types. if (matchingMethod == null) { @@ -132,8 +130,8 @@ protected Method findMatchingMethod() { * @param arguments the argument values to match against method parameters * @return a matching method, or {@code null} if none */ - @Nullable - protected Method doFindMatchingMethod(Object[] arguments) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + protected @Nullable Method doFindMatchingMethod(@Nullable Object[] arguments) { TypeConverter converter = getTypeConverter(); if (converter != null) { String targetMethod = getTargetMethod(); @@ -143,14 +141,14 @@ protected Method doFindMatchingMethod(Object[] arguments) { Assert.state(targetClass != null, "No target class set"); Method[] candidates = ReflectionUtils.getAllDeclaredMethods(targetClass); int minTypeDiffWeight = Integer.MAX_VALUE; - Object[] argumentsToUse = null; + @Nullable Object[] argumentsToUse = null; for (Method candidate : candidates) { if (candidate.getName().equals(targetMethod)) { // Check if the inspected method has the correct number of parameters. int parameterCount = candidate.getParameterCount(); if (parameterCount == argCount) { Class[] paramTypes = candidate.getParameterTypes(); - Object[] convertedArguments = new Object[argCount]; + @Nullable Object[] convertedArguments = new Object[argCount]; boolean match = true; for (int j = 0; j < argCount && match; j++) { // Verify that the supplied argument is assignable to the method parameter. diff --git a/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java b/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java index 495072fa36e5..e6f2835613ea 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/MutableSortDefinition.java @@ -18,7 +18,8 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** diff --git a/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java b/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java index 063834e1a8d6..161620a403b7 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/PagedListHolder.java @@ -22,7 +22,8 @@ import java.util.Date; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -66,14 +67,11 @@ public class PagedListHolder implements Serializable { private List source = Collections.emptyList(); - @Nullable - private Date refreshDate; + private @Nullable Date refreshDate; - @Nullable - private SortDefinition sort; + private @Nullable SortDefinition sort; - @Nullable - private SortDefinition sortUsed; + private @Nullable SortDefinition sortUsed; private int pageSize = DEFAULT_PAGE_SIZE; @@ -134,8 +132,7 @@ public List getSource() { /** * Return the last time the list has been fetched from the source provider. */ - @Nullable - public Date getRefreshDate() { + public @Nullable Date getRefreshDate() { return this.refreshDate; } @@ -151,8 +148,7 @@ public void setSort(@Nullable SortDefinition sort) { /** * Return the sort definition for this holder. */ - @Nullable - public SortDefinition getSort() { + public @Nullable SortDefinition getSort() { return this.sort; } diff --git a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java index 9a9a7d8f30d1..ad0c58bbcdbe 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/PropertyComparator.java @@ -23,10 +23,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.BeansException; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -108,8 +108,7 @@ public int compare(T o1, T o2) { * @param obj the object to get the property value for * @return the property value */ - @Nullable - private Object getPropertyValue(Object obj) { + private @Nullable Object getPropertyValue(Object obj) { // If a nested property cannot be read, simply return null // (similar to JSTL EL). If the property doesn't exist in the // first place, let the exception through. diff --git a/spring-beans/src/main/java/org/springframework/beans/support/package-info.java b/spring-beans/src/main/java/org/springframework/beans/support/package-info.java index 326ce25e1448..73ea6d22c9ac 100644 --- a/spring-beans/src/main/java/org/springframework/beans/support/package-info.java +++ b/spring-beans/src/main/java/org/springframework/beans/support/package-info.java @@ -2,9 +2,7 @@ * Classes supporting the org.springframework.beans package, * such as utility classes for sorting and holding lists of beans. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 new file mode 100644 index 000000000000..cb589094595f --- /dev/null +++ b/spring-beans/src/main/kotlin/org/springframework/beans/factory/BeanRegistrarDsl.kt @@ -0,0 +1,1109 @@ +/* + * Copyright 2002-2025 the original author 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.beans.factory + +import org.springframework.beans.factory.BeanRegistry.SupplierContext +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.ResolvableType +import org.springframework.core.env.Environment + +/** + * Contract for registering programmatically beans. + * + * Typically imported with an `@Import` annotation on `@Configuration` classes. + * ``` + * @Configuration + * @Import(MyBeanRegistrar::class) + * class MyConfiguration { + * } + * ``` + * + * In Kotlin, a bean registrar is typically created with a `BeanRegistrarDsl` to register + * beans programmatically in a concise and flexible way. + * ``` + * class MyBeanRegistrar : BeanRegistrarDsl({ + * registerBean() + * registerBean( + * name = "bar", + * prototype = true, + * lazyInit = true, + * description = "Custom description") { + * Bar(bean()) + * } + * profile("baz") { + * registerBean { Baz("Hello World!") } + * } + * }) + * ``` + * + * @author Sebastien Deleuze + * @since 7.0 + */ +@BeanRegistrarDslMarker +open class BeanRegistrarDsl(private val init: BeanRegistrarDsl.() -> Unit): BeanRegistrar { + + @PublishedApi + internal lateinit var registry: BeanRegistry + + /** + * The environment that can be used to get the active profile or some properties. + */ + lateinit var env: Environment + + + /** + * Apply the nested block if the given profile expression matches the + * active profiles. + * + * A profile expression may contain a simple profile name (for example + * `"production"`) or a compound expression. A compound expression allows + * for more complicated profile logic to be expressed, for example + * `"production & cloud"`. + * + * The following operators are supported in profile expressions: + * - `!` - A logical *NOT* of the profile name or compound expression + * - `&` - A logical *AND* of the profile names or compound expressions + * - `|` - A logical *OR* of the profile names or compound expressions + * + * Please note that the `&` and `|` operators may not be mixed + * without using parentheses. For example, `"a & b | c"` is not a valid + * expression: it must be expressed as `"(a & b) | c"` or `"a & (b | c)"`. + * @param expression the profile expressions to evaluate + */ + fun profile(expression: String, init: BeanRegistrarDsl.() -> Unit) { + if (env.matchesProfiles(expression)) { + init() + } + } + + /** + * Register beans using the given [BeanRegistrar]. + * @param registrar the bean registrar that will be called to register + * additional beans + */ + fun register(registrar: BeanRegistrar) { + return registry.register(registrar) + } + + /** + * Given a name, register an alias for it. + * @param name the canonical name + * @param alias the alias to be registered + * @throws IllegalStateException if the alias is already in use + * and may not be overridden + */ + fun registerAlias(name: String, alias: String) { + registry.registerAlias(name, alias); + } + + /** + * 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 + * @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(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) { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); + if (resolvableType.hasGenerics()) { + it.targetType(resolvableType) + } + } + registry.registerBean(name, T::class.java, customizer) + } + + /** + * 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 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 + * @return the generated bean name + */ + inline fun registerBean(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): String { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); + if (resolvableType.hasGenerics()) { + it.targetType(resolvableType) + } + } + return registry.registerBean(T::class.java, customizer) + } + + /** + * 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 + * 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 + * @param supplier the supplier to construct a bean instance + */ + inline fun registerBean(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, + crossinline supplier: (SupplierContextDsl.() -> T)) { + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (fallback) { + it.fallback() + } + if (infrastructure) { + it.infrastructure() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + it.supplier { + SupplierContextDsl(it, env).supplier() + } + val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); + if (resolvableType.hasGenerics()) { + it.targetType(resolvableType) + } + } + registry.registerBean(name, T::class.java, customizer) + } + + inline fun registerBean(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, + crossinline supplier: (SupplierContextDsl.() -> T)): String { + /** + * 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 + * @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 + * @param supplier the supplier to construct a bean instance + */ + + val customizer: (BeanRegistry.Spec) -> Unit = { + if (!autowirable) { + it.notAutowirable() + } + if (backgroundInit) { + it.backgroundInit() + } + if (description != null) { + it.description(description) + } + if (infrastructure) { + it.infrastructure() + } + if (fallback) { + it.fallback() + } + if (lazyInit) { + it.lazyInit() + } + if (order != null) { + it.order(order) + } + if (primary) { + it.primary() + } + if (prototype) { + it.prototype() + } + it.supplier { + SupplierContextDsl(it, env).supplier() + } + val resolvableType = ResolvableType.forType(object: ParameterizedTypeReference() {}); + if (resolvableType.hasGenerics()) { + it.targetType(resolvableType) + } + } + 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, val env: Environment) { + + /** + * Return the bean instance that uniquely matches the given object type, + * and potentially the name if provided, if any. + * @param T the bean type + * @param name the name of the bean + */ + inline fun bean(name: String? = null) : T = when (name) { + null -> beanProvider().getObject() + else -> context.bean(name, T::class.java) + } + + /** + * Return a provider for the specified bean, allowing for lazy on-demand + * retrieval of instances, including availability and uniqueness options. + * @param T type the bean must match; can be an interface or superclass + * @return a corresponding provider handle + */ + inline fun beanProvider() : ObjectProvider = + context.beanProvider(ResolvableType.forType((object : ParameterizedTypeReference() {}).type)) + } + + override fun register(registry: BeanRegistry, env: Environment) { + this.registry = registry + this.env = env + init() + } + +} + +@DslMarker +internal annotation class BeanRegistrarDslMarker diff --git a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java index f656c446daea..ac2387fe7746 100644 --- a/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/AbstractPropertyAccessorTests.java @@ -34,6 +34,7 @@ import java.util.TreeMap; import java.util.TreeSet; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowire; @@ -49,7 +50,6 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java index b700314ba1cf..cd4bcf9e8ea5 100644 --- a/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/BeanUtilsTests.java @@ -33,6 +33,7 @@ import java.util.Locale; import java.util.UUID; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -46,7 +47,6 @@ import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceEditor; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -868,13 +868,11 @@ public BeanWithNullableTypes(@Nullable Integer counter, @Nullable Boolean flag, this.value = value; } - @Nullable - public Integer getCounter() { + public @Nullable Integer getCounter() { return counter; } - @Nullable - public Boolean isFlag() { + public @Nullable Boolean isFlag() { return flag; } 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 4238da225ea9..1f6b9f2414de 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 @@ -39,6 +39,7 @@ import java.util.stream.Stream; import jakarta.annotation.Priority; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.BeansException; @@ -76,7 +77,6 @@ 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; @@ -87,7 +87,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.UrlResource; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; import static org.assertj.core.api.Assertions.assertThat; @@ -263,6 +262,32 @@ void nonInitializedFactoryBeanIgnoredByNonEagerTypeMatching() { 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 @@ void autowireWithTwoMatchesForConstructorDependency() { 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 @@ void autowirePreferredConstructors() { 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 @@ void autowirePreferredConstructorsFromAttribute() { 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 @@ void autowirePreferredConstructorFromAttribute() throws Exception { 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"); @@ -1519,34 +1540,34 @@ void orderFromAttribute() { bd2.setBeanClass(DerivedTestBean.class); bd2.setPropertyValues(new MutablePropertyValues(List.of(new PropertyValue("name", "highest")))); bd2.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.HIGHEST_PRECEDENCE); + bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); lbf.registerBeanDefinition("bean2", bd2); assertThat(lbf.getBeanProvider(TestBean.class).orderedStream().map(TestBean::getName)) .containsExactly("highest", "lowest"); - assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED).map(TestBean::getName)) - .containsExactly("highest", "lowest"); assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(clazz -> !DerivedTestBean.class.isAssignableFrom(clazz)) .map(TestBean::getName)).containsExactly("lowest"); + assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED).map(TestBean::getName)) + .containsExactly("highest", "lowest"); + assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED, false).map(TestBean::getName)) + .containsExactly("lowest"); } @Test - void orderFromAttributeOverrideAnnotation() { + void orderFromAttributeOverridesAnnotation() { lbf.setDependencyComparator(AnnotationAwareOrderComparator.INSTANCE); RootBeanDefinition rbd1 = new RootBeanDefinition(LowestPrecedenceTestBeanFactoryBean.class); rbd1.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.HIGHEST_PRECEDENCE); lbf.registerBeanDefinition("lowestPrecedenceFactory", rbd1); RootBeanDefinition rbd2 = new RootBeanDefinition(HighestPrecedenceTestBeanFactoryBean.class); rbd2.setAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE, Ordered.LOWEST_PRECEDENCE); + rbd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); lbf.registerBeanDefinition("highestPrecedenceFactory", rbd2); - GenericBeanDefinition bd1 = new GenericBeanDefinition(); - bd1.setFactoryBeanName("highestPrecedenceFactory"); - lbf.registerBeanDefinition("bean1", bd1); - GenericBeanDefinition bd2 = new GenericBeanDefinition(); - bd2.setFactoryBeanName("lowestPrecedenceFactory"); - lbf.registerBeanDefinition("bean2", bd2); assertThat(lbf.getBeanProvider(TestBean.class).orderedStream().map(TestBean::getName)) .containsExactly("fromLowestPrecedenceTestBeanFactoryBean", "fromHighestPrecedenceTestBeanFactoryBean"); assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED).map(TestBean::getName)) .containsExactly("fromLowestPrecedenceTestBeanFactoryBean", "fromHighestPrecedenceTestBeanFactoryBean"); + assertThat(lbf.getBeanProvider(TestBean.class).orderedStream(ObjectProvider.UNFILTERED, false).map(TestBean::getName)) + .containsExactly("fromLowestPrecedenceTestBeanFactoryBean"); } @Test @@ -1987,7 +2008,6 @@ void getBeanByTypeInstanceDefinedInParent() { void getBeanByTypeInstanceWithAmbiguity() { RootBeanDefinition bd1 = createConstructorDependencyBeanDefinition(99); RootBeanDefinition bd2 = new RootBeanDefinition(ConstructorDependency.class); - bd2.setScope(BeanDefinition.SCOPE_PROTOTYPE); bd2.getConstructorArgumentValues().addGenericArgumentValue("43"); lbf.registerBeanDefinition("bd1", bd1); lbf.registerBeanDefinition("bd2", bd2); @@ -2028,6 +2048,10 @@ void getBeanByTypeInstanceWithAmbiguity() { assertThat(resolved).hasSize(2); assertThat(resolved).contains(lbf.getBean("bd1")); assertThat(resolved).contains(lbf.getBean("bd2")); + + resolved = provider.stream(ObjectProvider.UNFILTERED, false).collect(Collectors.toSet()); + assertThat(resolved).hasSize(1); + assertThat(resolved).contains(lbf.getBean("bd2")); } @Test @@ -2082,6 +2106,9 @@ void getBeanByTypeInstanceWithPrimary() { assertThat(resolved).hasSize(2); assertThat(resolved).contains(lbf.getBean("bd1")); assertThat(resolved).contains(lbf.getBean("bd2")); + + resolved = provider.stream(ObjectProvider.UNFILTERED, false).collect(Collectors.toSet()); + assertThat(resolved).isEmpty(); } @Test 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 f53f9ff5a48a..03c9e1139a89 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 @@ -40,6 +40,7 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; @@ -76,7 +77,6 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.Order; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -960,6 +960,33 @@ void constructorResourceInjectionWithMultipleCandidates() { @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 @@ void singleConstructorInjectionWithEmptyCollectionAsNull() { @Test void singleConstructorInjectionWithMissingDependency() { bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(SingleConstructorOptionalCollectionBean.class)); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> bf.getBean("annotatedBean")); } @@ -1203,6 +1231,7 @@ void singleConstructorInjectionWithNullDependency() { RootBeanDefinition tb = new RootBeanDefinition(NullFactoryMethods.class); tb.setFactoryMethodName("createTestBean"); bf.registerBeanDefinition("testBean", tb); + assertThatExceptionOfType(UnsatisfiedDependencyException.class) .isThrownBy(() -> bf.getBean("annotatedBean")); } @@ -1614,6 +1643,10 @@ void objectProviderInjectionWithPrototype() { assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class)); testBeans = bean.allTestBeansInOrder(); assertThat(testBeans).containsExactly(bf.getBean("testBean1", TestBean.class), bf.getBean("testBean2", TestBean.class)); + testBeans = bean.allSingletonBeans(); + assertThat(testBeans).isEmpty(); + testBeans = bean.allSingletonBeansInOrder(); + assertThat(testBeans).isEmpty(); } @Test @@ -1648,6 +1681,12 @@ void objectProviderInjectionWithSingletonTarget() { testBeans = bean.allTestBeansInOrder(); assertThat(testBeans).hasSize(1); assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); + testBeans = bean.allSingletonBeans(); + assertThat(testBeans).hasSize(1); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); + testBeans = bean.allSingletonBeansInOrder(); + assertThat(testBeans).hasSize(1); + assertThat(testBeans).contains(bf.getBean("testBean", TestBean.class)); } @Test @@ -1675,6 +1714,10 @@ void objectProviderInjectionWithTargetNotAvailable() { assertThat(testBeans).isEmpty(); testBeans = bean.allTestBeansInOrder(); assertThat(testBeans).isEmpty(); + testBeans = bean.allSingletonBeans(); + assertThat(testBeans).isEmpty(); + testBeans = bean.allSingletonBeansInOrder(); + assertThat(testBeans).isEmpty(); } @Test @@ -1698,6 +1741,8 @@ void objectProviderInjectionWithTargetNotUnique() { assertThat(bean.streamTestBeansInOrder()).containsExactly(testBean1, testBean2); assertThat(bean.allTestBeans()).containsExactly(testBean1, testBean2); assertThat(bean.allTestBeansInOrder()).containsExactly(testBean1, testBean2); + assertThat(bean.allSingletonBeans()).containsExactly(testBean1, testBean2); + assertThat(bean.allSingletonBeansInOrder()).containsExactly(testBean1, testBean2); } @Test @@ -1711,6 +1756,7 @@ void objectProviderInjectionWithTargetPrimary() { 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); @@ -1728,6 +1774,41 @@ void objectProviderInjectionWithTargetPrimary() { 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); + 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 @@ -1739,7 +1820,7 @@ void objectProviderInjectionWithUnresolvedOrderedStream() { bf.registerBeanDefinition("testBean1", tb1); RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class); tb2.setFactoryMethodName("newTestBean2"); - tb2.setLazyInit(true); + tb2.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("testBean2", tb2); ObjectProviderInjectionBean bean = bf.getBean("annotatedBean", ObjectProviderInjectionBean.class); @@ -1747,6 +1828,7 @@ void objectProviderInjectionWithUnresolvedOrderedStream() { bf.getBean("testBean1", TestBean.class)); assertThat(bean.allTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class), bf.getBean("testBean1", TestBean.class)); + assertThat(bean.allSingletonBeansInOrder()).containsExactly(bf.getBean("testBean1", TestBean.class)); } @Test @@ -1757,6 +1839,7 @@ void objectProviderInjectionWithNonCandidatesInStream() { bf.registerBeanDefinition("testBean1", tb1); RootBeanDefinition tb2 = new RootBeanDefinition(TestBeanFactory.class); tb2.setFactoryMethodName("newTestBean2"); + tb2.setScope(BeanDefinition.SCOPE_PROTOTYPE); bf.registerBeanDefinition("testBean2", tb2); DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); @@ -1789,6 +1872,10 @@ void objectProviderInjectionWithNonCandidatesInStream() { bf.getBean("testBean2", TestBean.class), bf.getBean("testBean4", TestBean.class)); assertThat(bean.allTestBeansInOrder()).containsExactly(bf.getBean("testBean2", TestBean.class), bf.getBean("testBean1", TestBean.class), bf.getBean("testBean4", TestBean.class)); + assertThat(bean.allSingletonBeans()).containsExactly(bf.getBean("testBean1", TestBean.class), + bf.getBean("testBean4", TestBean.class)); + assertThat(bean.allSingletonBeansInOrder()).containsExactly(bf.getBean("testBean1", TestBean.class), + bf.getBean("testBean4", TestBean.class)); Map typeMatches = BeanFactoryUtils.beansOfTypeIncludingAncestors(bf, TestBean.class); assertThat(typeMatches.remove("testBean3")).isNotNull(); @@ -2370,7 +2457,7 @@ void genericsBasedConstructorInjection() { } @Test - @SuppressWarnings({ "rawtypes", "unchecked" }) + @SuppressWarnings({ "unchecked", "rawtypes" }) void genericsBasedConstructorInjectionWithNonTypedTarget() { RootBeanDefinition bd = new RootBeanDefinition(RepositoryConstructorInjectionBean.class); bd.setScope(BeanDefinition.SCOPE_PROTOTYPE); @@ -2650,22 +2737,22 @@ private void testBeanQualifierProvider() {} 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; } } @@ -2674,13 +2761,13 @@ public TestBean getTestBean2() { 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; @@ -2689,18 +2776,18 @@ public NonPublicResourceInjectionBean() { @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; } @@ -2710,11 +2797,11 @@ protected void initBeanFactory(BeanFactory beanFactory) { this.beanFactory = beanFactory; } - public ITestBean getTestBean3() { + public @Nullable ITestBean getTestBean3() { return this.testBean3; } - public ITestBean getTestBean4() { + public @Nullable ITestBean getTestBean4() { return this.testBean4; } @@ -3002,7 +3089,6 @@ public static class ConstructorWithoutFallbackBean { protected ITestBean testBean3; - @Autowired(required = false) public ConstructorWithoutFallbackBean(ITestBean testBean3) { this.testBean3 = testBean3; } @@ -3017,7 +3103,6 @@ public static class ConstructorWithNullableArgument { protected ITestBean testBean3; - @Autowired(required = false) public ConstructorWithNullableArgument(@Nullable ITestBean testBean3) { this.testBean3 = testBean3; } @@ -3393,6 +3478,14 @@ public List allTestBeans() { public List allTestBeansInOrder() { return this.testBean.orderedStream(ObjectProvider.UNFILTERED).toList(); } + + public List allSingletonBeans() { + return this.testBean.stream(ObjectProvider.UNFILTERED, false).toList(); + } + + public List allSingletonBeansInOrder() { + return this.testBean.orderedStream(ObjectProvider.UNFILTERED, false).toList(); + } } @@ -4380,8 +4473,7 @@ public static TestBean newTestBean2() { static class MixedNullableInjectionBean { - @Nullable - public Integer nullableBean; + public @Nullable Integer nullableBean; public String nonNullBean; @@ -4395,8 +4487,7 @@ public void nullabilityInjection(@Nullable Integer nullableBean, String nonNullB static class MixedOptionalInjectionBean { - @Nullable - public Integer nullableBean; + public @Nullable Integer nullableBean; public String nonNullBean; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java index 0e38a02da29c..017074e50240 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/AutowiredAnnotationBeanRegistrationAotContributionTests.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,7 +94,7 @@ void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { RegisteredBean registeredBean = getAndApplyContribution( PrivateFieldInjectionSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PrivateFieldInjectionSample.class, "environment")) + .onType(PrivateFieldInjectionSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateFieldInjectionSample instance = new PrivateFieldInjectionSample(); @@ -113,7 +113,7 @@ void contributeWhenPackagePrivateFieldInjectionInjectsUsingConsumer() { RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateFieldInjectionSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PackagePrivateFieldInjectionSample.class, "environment")) + .onType(PackagePrivateFieldInjectionSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateFieldInjectionSample instance = new PackagePrivateFieldInjectionSample(); @@ -132,7 +132,7 @@ void contributeWhenPackagePrivateFieldInjectionOnParentClassInjectsUsingReflecti RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateFieldInjectionFromParentSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PackagePrivateFieldInjectionSample.class, "environment")) + .onType(PackagePrivateFieldInjectionSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateFieldInjectionFromParentSample instance = new PackagePrivateFieldInjectionFromParentSample(); @@ -150,7 +150,7 @@ void contributeWhenPrivateMethodInjectionInjectsUsingReflection() { RegisteredBean registeredBean = getAndApplyContribution( PrivateMethodInjectionSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PrivateMethodInjectionSample.class, "setTestBean").invoke()) + .onMethodInvocation(PrivateMethodInjectionSample.class, "setTestBean")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateMethodInjectionSample instance = new PrivateMethodInjectionSample(); @@ -169,7 +169,7 @@ void contributeWhenPackagePrivateMethodInjectionInjectsUsingConsumer() { RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateMethodInjectionSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PackagePrivateMethodInjectionSample.class, "setTestBean").introspect()) + .onType(PackagePrivateMethodInjectionSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateMethodInjectionSample instance = new PackagePrivateMethodInjectionSample(); @@ -188,7 +188,7 @@ void contributeWhenPackagePrivateMethodInjectionOnParentClassInjectsUsingReflect RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateMethodInjectionFromParentSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PackagePrivateMethodInjectionSample.class, "setTestBean")) + .onMethodInvocation(PackagePrivateMethodInjectionSample.class, "setTestBean")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateMethodInjectionFromParentSample instance = new PackagePrivateMethodInjectionFromParentSample(); diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java index ef2e236fb689..371e93013d42 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/JakartaAnnotationsRuntimeHintsTests.java @@ -63,14 +63,4 @@ void jakartaQualifierAnnotationHasHints() { assertThat(RuntimeHintsPredicates.reflection().onType(Qualifier.class)).accepts(this.hints); } - @Test // gh-33345 - void javaxInjectAnnotationHasHints() { - assertThat(RuntimeHintsPredicates.reflection().onType(javax.inject.Inject.class)).accepts(this.hints); - } - - @Test // gh-33345 - void javaxQualifierAnnotationHasHints() { - assertThat(RuntimeHintsPredicates.reflection().onType(javax.inject.Qualifier.class)).accepts(this.hints); - } - } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java index e2afff6ee817..1ae68d0296ab 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/annotation/ParameterResolutionTests.java @@ -71,8 +71,8 @@ void annotatedParametersInInnerClassConstructorAreCandidatesForAutowiring() thro } private void assertAutowirableParameters(Executable executable) { - int startIndex = (executable instanceof Constructor) - && ClassUtils.isInnerClass(executable.getDeclaringClass()) ? 1 : 0; + int startIndex = (executable instanceof Constructor) && + ClassUtils.isInnerClass(executable.getDeclaringClass()) ? 1 : 0; Parameter[] parameters = executable.getParameters(); for (int parameterIndex = startIndex; parameterIndex < parameters.length; parameterIndex++) { Parameter parameter = parameters[parameterIndex]; 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 7f35baca3231..e53314ac6342 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 @@ void generateCodeWhenMultipleArgumentsWithOffset() { 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/BeanDefinitionMethodGeneratorFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java index 7f38730c03f3..154b417078b9 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionMethodGeneratorFactoryTests.java @@ -16,6 +16,7 @@ package org.springframework.beans.factory.aot; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -24,7 +25,6 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.Ordered; import org.springframework.core.test.io.support.MockSpringFactoriesLoader; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -205,8 +205,7 @@ static class MockBeanRegistrationExcludeFilter implements private final int order; - @Nullable - private RegisteredBean registeredBean; + private @Nullable RegisteredBean registeredBean; MockBeanRegistrationExcludeFilter(boolean excluded, int order) { this.excluded = excluded; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java index 2fc485a5abd7..ee48e20a0602 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertiesCodeGeneratorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import javax.lang.model.element.Modifier; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; @@ -58,7 +59,6 @@ import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; @@ -568,13 +568,13 @@ private void assertReflectionOnPublisher() { private void assertHasMethodInvokeHints(Class beanType, String... methodNames) { assertThat(methodNames).allMatch(methodName -> RuntimeHintsPredicates.reflection() - .onMethod(beanType, methodName).invoke() + .onMethodInvocation(beanType, methodName) .test(this.generationContext.getRuntimeHints())); } private void assertHasDeclaredFieldsHint(Class beanType) { assertThat(RuntimeHintsPredicates.reflection() - .onType(beanType).withMemberCategory(MemberCategory.DECLARED_FIELDS)) + .onType(beanType).withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)) .accepts(this.generationContext.getRuntimeHints()); } @@ -706,15 +706,13 @@ public void setName(String name) { this.name = name; } - @Nullable @Override - public String getObject() { + public @Nullable String getObject() { return getPrefix() + " " + getName(); } - @Nullable @Override - public Class getObjectType() { + public @Nullable Class getObjectType() { return String.class; } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanDefinitionPropertyValueCodeGeneratorDelegatesTests.java index 0dafc56c1a23..d37bd4b6ae37 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 @@ 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.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$$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 @@ 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 @@ private void compile(Object value, BiConsumer result) { CodeBlock generatedCode = createValueCodeGenerator(generatedClass).generateCode(value); typeBuilder.set(type -> { type.addModifiers(Modifier.PUBLIC); - type.addSuperinterface( - ParameterizedTypeName.get(Supplier.class, Object.class)); - type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC) + type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC, Modifier.STATIC) .returns(Object.class).addStatement("return $L", generatedCode).build()); }); generationContext.writeGeneratedContent(); TestCompiler.forSystem().with(generationContext).compile(compiled -> - result.accept(compiled.getInstance(Supplier.class).get(), compiled)); + result.accept(getGeneratedCodeReturnValue(compiled, generatedClass), compiled)); + } + + private static Object getGeneratedCodeReturnValue(Compiled compiled, GeneratedClass generatedClass) { + try { + Object instance = compiled.getInstance(Object.class, generatedClass.getName().reflectionName()); + Method get = ReflectionUtils.findMethod(instance.getClass(), "get"); + return get.invoke(null); + } + catch (Exception ex) { + throw new RuntimeException("Failed to invoke generated code '%s':".formatted(generatedClass.getName()), ex); + } } @Nested diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanInstanceSupplierTests.java index 078bbee216c5..cae352390047 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.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; @@ -62,7 +63,6 @@ import org.springframework.util.ReflectionUtils; import org.springframework.util.function.ThrowingBiFunction; import org.springframework.util.function.ThrowingFunction; -import org.springframework.util.function.ThrowingSupplier; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -103,8 +103,8 @@ void forConstructorWhenNotFoundThrowsException() { RegisteredBean registerBean = source.registerBean(this.beanFactory); assertThatIllegalArgumentException() .isThrownBy(() -> resolver.get(registerBean)).withMessage( - "Constructor with parameter types [java.io.InputStream] cannot be found on " - + SingleArgConstructor.class.getName()); + "Constructor with parameter types [java.io.InputStream] cannot be found on " + + SingleArgConstructor.class.getName()); } @Test @@ -151,8 +151,8 @@ void forFactoryMethodWhenNotFoundThrowsException() { RegisteredBean registerBean = source.registerBean(this.beanFactory); assertThatIllegalArgumentException() .isThrownBy(() -> resolver.get(registerBean)).withMessage( - "Factory method 'single' with parameter types [java.io.InputStream] declared on class " - + SingleArgFactory.class.getName() + " cannot be found"); + "Factory method 'single' with parameter types [java.io.InputStream] declared on class " + + SingleArgFactory.class.getName() + " cannot be found"); } @Test @@ -180,16 +180,6 @@ void withGeneratorWhenFunctionIsNullThrowsException() { .withMessage("'generator' must not be null"); } - @Test - @Deprecated - @SuppressWarnings("removal") - void withGeneratorWhenSupplierIsNullThrowsException() { - BeanInstanceSupplier resolver = BeanInstanceSupplier.forConstructor(); - assertThatIllegalArgumentException() - .isThrownBy(() -> resolver.withGenerator((ThrowingSupplier) null)) - .withMessage("'generator' must not be null"); - } - @Test void getWithConstructorDoesNotSetResolvedFactoryMethod() { BeanInstanceSupplier resolver = BeanInstanceSupplier.forConstructor(String.class); @@ -236,18 +226,6 @@ void getWithGeneratorCallsFunction() { assertThat(resolver.get(registerBean)).isInstanceOf(String.class).isEqualTo("1"); } - @Test - @Deprecated - @SuppressWarnings("removal") - void getWithGeneratorCallsSupplier() { - BeanRegistrar registrar = new BeanRegistrar(SingleArgConstructor.class); - this.beanFactory.registerSingleton("one", "1"); - RegisteredBean registerBean = registrar.registerBean(this.beanFactory); - BeanInstanceSupplier resolver = BeanInstanceSupplier.forConstructor(String.class) - .withGenerator(() -> "1"); - assertThat(resolver.get(registerBean)).isInstanceOf(String.class).isEqualTo("1"); - } - @Test void getWhenRegisteredBeanIsNullThrowsException() { BeanInstanceSupplier resolver = BeanInstanceSupplier.forConstructor(String.class); @@ -691,7 +669,7 @@ public void accept(ParameterizedResolverTest annotation) { } @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/BeanRegistrationsAotContributionTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java index 2cccc827f476..494a809cc629 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/BeanRegistrationsAotContributionTests.java @@ -31,7 +31,6 @@ import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; import org.springframework.aot.generate.ValueCodeGenerationException; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationsAotContribution.Registration; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -149,14 +148,11 @@ void applyToRegisterReflectionHints() { registeredBean, null, List.of()); BeanRegistrationsAotContribution contribution = createContribution(registeredBean, generator); contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode); - assertThat(reflection().onType(Employee.class) - .withMemberCategories(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS)) + assertThat(reflection().onType(Employee.class)) .accepts(this.generationContext.getRuntimeHints()); - assertThat(reflection().onType(ITestBean.class) - .withMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS)) + assertThat(reflection().onType(ITestBean.class)) .accepts(this.generationContext.getRuntimeHints()); - assertThat(reflection().onType(AgeHolder.class) - .withMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS)) + assertThat(reflection().onType(AgeHolder.class)) .accepts(this.generationContext.getRuntimeHints()); } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java index 7379fc22262e..1fd472838a30 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/aot/DefaultBeanRegistrationCodeFragmentsTests.java @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import java.util.function.UnaryOperator; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; @@ -44,7 +45,6 @@ import org.springframework.core.ResolvableType; import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -86,8 +86,8 @@ public void getTargetWithInstanceSupplierAndResourceDescription() { BeanRegistrationCodeFragments codeFragments = createInstance(registeredBean); assertThatExceptionOfType(AotBeanProcessingException.class) .isThrownBy(() -> codeFragments.getTarget(registeredBean)) - .withMessageContaining("Error processing bean with name 'testBean' defined in my test resource: " - + "instance supplier is not supported"); + .withMessageContaining("Error processing bean with name 'testBean' defined in my test resource: " + + "instance supplier is not supported"); } @Test 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 f18a55c02c0b..cddfb5dd2b60 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. @@ -100,8 +100,7 @@ void generateWhenHasDefaultConstructor() { assertThat(compiled.getSourceFile()) .contains("InstanceSupplier.using(TestBean::new)"); }); - assertThat(getReflectionHints().getTypeHint(TestBean.class)) - .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(TestBean.class)).isNotNull(); } @Test @@ -112,8 +111,7 @@ void generateWhenHasConstructorWithParameter() { InjectionComponent bean = getBean(beanDefinition, instanceSupplier); assertThat(bean).isInstanceOf(InjectionComponent.class).extracting("bean").isEqualTo("injected"); }); - assertThat(getReflectionHints().getTypeHint(InjectionComponent.class)) - .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(InjectionComponent.class)).isNotNull(); } @Test @@ -126,23 +124,23 @@ void generateWhenHasConstructorWithInnerClassAndDefaultConstructor() { assertThat(compiled.getSourceFile()).contains( "getBeanFactory().getBean(InnerComponentConfiguration.class).new NoDependencyComponent()"); }); - assertThat(getReflectionHints().getTypeHint(NoDependencyComponent.class)) - .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(NoDependencyComponent.class)).isNotNull(); } @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("); }); - assertThat(getReflectionHints().getTypeHint(EnvironmentAwareComponent.class)) - .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(EnvironmentAwareComponent.class)).isNotNull(); } @Test @@ -162,11 +160,13 @@ void generateWhenHasNonPublicConstructorWithInnerClassAndDefaultConstructor() { @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)"); }); @@ -184,8 +184,7 @@ void generateWhenHasConstructorWithGeneric() { assertThat(bean).extracting("number").isNull(); // No property actually set assertThat(compiled.getSourceFile()).contains("NumberHolderFactoryBean::new"); }); - assertThat(getReflectionHints().getTypeHint(NumberHolderFactoryBean.class)) - .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(NumberHolderFactoryBean.class)).isNotNull(); } @Test @@ -215,8 +214,7 @@ void generateWhenHasFactoryMethodWithNoArg() { assertThat(compiled.getSourceFile()).contains( "getBeanFactory().getBean(\"config\", SimpleConfiguration.class).stringBean()"); }); - assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) - .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)).isNotNull(); } @Test @@ -232,8 +230,7 @@ void generateWhenHasFactoryMethodOnInterface() { assertThat(compiled.getSourceFile()).contains( "getBeanFactory().getBean(\"config\", DefaultSimpleBeanContract.class).simpleBean()"); }); - assertThat(getReflectionHints().getTypeHint(SimpleBeanContract.class)) - .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(SimpleBeanContract.class)).isNotNull(); } @Test @@ -268,8 +265,7 @@ void generateWhenHasStaticFactoryMethodWithNoArg() { assertThat(compiled.getSourceFile()) .contains("(registeredBean) -> SimpleConfiguration.integerBean()"); }); - assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) - .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)).isNotNull(); } @Test @@ -287,8 +283,7 @@ void generateWhenHasStaticFactoryMethodWithArg() { assertThat(bean).isEqualTo("42test"); assertThat(compiled.getSourceFile()).contains("SampleFactory.create("); }); - assertThat(getReflectionHints().getTypeHint(SampleFactory.class)) - .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(SampleFactory.class)).isNotNull(); } @Test @@ -305,8 +300,7 @@ void generateWhenHasFactoryMethodCheckedException() { assertThat(bean).isEqualTo(42); assertThat(compiled.getSourceFile()).doesNotContain(") throws Exception {"); }); - assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)) - .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)); + assertThat(getReflectionHints().getTypeHint(SimpleConfiguration.class)).isNotNull(); } 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 01d387a95aba..340e7692c92d 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 @@ * @author Chris Beams * @author Sam Brannen */ -@SuppressWarnings("deprecation") +@SuppressWarnings({"deprecation", "removal"}) class PropertyPlaceholderConfigurerTests { private static final String P1 = "p1"; 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 5c764fa87c53..23a878332d13 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 @@ * @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/BeanRegistryAdapterTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java new file mode 100644 index 000000000000..36abaa4ca63a --- /dev/null +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/BeanRegistryAdapterTests.java @@ -0,0 +1,342 @@ +/* + * Copyright 2002-2025 the original author 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.beans.factory.support; + +import java.util.function.Supplier; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.ResolvableType; +import org.springframework.core.env.Environment; +import org.springframework.core.env.StandardEnvironment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link BeanRegistryAdapter}. + * + * @author Sebastien Deleuze + */ +public class BeanRegistryAdapterTests { + + private final DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + + private final Environment env = new StandardEnvironment(); + + @Test + void defaultBackgroundInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isBackgroundInit()).isFalse(); + } + + @Test + void enableBackgroundInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, BackgroundInitBeanRegistrar.class); + new BackgroundInitBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isBackgroundInit()).isTrue(); + } + + @Test + void defaultDescription() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getDescription()).isNull(); + } + + @Test + void customDescription() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, CustomDescriptionBeanRegistrar.class); + new CustomDescriptionBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getDescription()).isEqualTo("custom"); + } + + @Test + void defaultFallback() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isFallback()).isFalse(); + } + + @Test + void enableFallback() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, FallbackBeanRegistrar.class); + new FallbackBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isFallback()).isTrue(); + } + + @Test + void defaultRole() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getRole()).isEqualTo(AbstractBeanDefinition.ROLE_APPLICATION); + } + + @Test + void infrastructureRole() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, InfrastructureBeanRegistrar.class); + new InfrastructureBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getRole()).isEqualTo(AbstractBeanDefinition.ROLE_INFRASTRUCTURE); + } + + @Test + void defaultLazyInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isLazyInit()).isFalse(); + } + + @Test + void enableLazyInit() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, LazyInitBeanRegistrar.class); + new LazyInitBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isLazyInit()).isTrue(); + } + + @Test + void defaultAutowirable() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isAutowireCandidate()).isTrue(); + } + + @Test + void notAutowirable() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, NotAutowirableBeanRegistrar.class); + new NotAutowirableBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isAutowireCandidate()).isFalse(); + } + + @Test + void defaultOrder() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + Integer order = (Integer)beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + assertThat(order).isNull(); + } + + @Test + void customOrder() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, CustomOrderBeanRegistrar.class); + new CustomOrderBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition) this.beanFactory.getBeanDefinition("foo"); + Integer order = (Integer)beanDefinition.getAttribute(AbstractBeanDefinition.ORDER_ATTRIBUTE); + assertThat(order).isEqualTo(1); + } + + @Test + void defaultPrimary() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isPrimary()).isFalse(); + } + + @Test + void enablePrimary() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, PrimaryBeanRegistrar.class); + new PrimaryBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.isPrimary()).isTrue(); + } + + @Test + void defaultScope() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getScope()).isEqualTo(AbstractBeanDefinition.SCOPE_DEFAULT); + } + + @Test + void prototypeScope() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, PrototypeBeanRegistrar.class); + new PrototypeBeanRegistrar().register(adapter, env); + BeanDefinition beanDefinition = this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getScope()).isEqualTo(AbstractBeanDefinition.SCOPE_PROTOTYPE); + } + + @Test + void defaultSupplier() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, DefaultBeanRegistrar.class); + new DefaultBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)this.beanFactory.getBeanDefinition("foo"); + assertThat(beanDefinition.getInstanceSupplier()).isNull(); + } + + @Test + void customSupplier() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, SupplierBeanRegistrar.class); + new SupplierBeanRegistrar().register(adapter, env); + AbstractBeanDefinition beanDefinition = (AbstractBeanDefinition)this.beanFactory.getBeanDefinition("foo"); + Supplier supplier = beanDefinition.getInstanceSupplier(); + assertThat(supplier).isNotNull(); + assertThat(supplier.get()).isNotNull().isInstanceOf(Foo.class); + } + + @Test + void customTargetTypeFromResolvableType() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, env, TargetTypeBeanRegistrar.class); + new TargetTypeBeanRegistrar().register(adapter, env); + RootBeanDefinition beanDefinition = (RootBeanDefinition)this.beanFactory.getBeanDefinition("fooSupplierFromResolvableType"); + assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(Foo.class); + } + + @Test + void customTargetTypeFromTypeReference() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, env, TargetTypeBeanRegistrar.class); + new TargetTypeBeanRegistrar().register(adapter, env); + RootBeanDefinition beanDefinition = (RootBeanDefinition)this.beanFactory.getBeanDefinition("fooSupplierFromTypeReference"); + assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(Foo.class); + } + + @Test + void registerViaAnotherRegistrar() { + BeanRegistryAdapter adapter = new BeanRegistryAdapter(this.beanFactory, this.beanFactory, this.env, ChainedBeanRegistrar.class); + new ChainedBeanRegistrar().register(adapter, env); + assertThat(this.beanFactory.getBeanDefinition("foo")).isNotNull(); + } + + + private static class ChainedBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.register(new DefaultBeanRegistrar()); + } + } + + private static class DefaultBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class); + } + } + + private static class BackgroundInitBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::backgroundInit); + } + } + + private static class CustomDescriptionBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, spec -> spec.description("custom")); + } + } + + private static class FallbackBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::fallback); + } + } + + private static class InfrastructureBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::infrastructure); + } + } + + private static class LazyInitBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::lazyInit); + } + } + + private static class NotAutowirableBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::notAutowirable); + } + } + + private static class CustomOrderBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, spec -> spec.order(1)); + } + } + + private static class PrimaryBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::primary); + } + } + + private static class PrototypeBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, BeanRegistry.Spec::prototype); + } + } + + private static class SupplierBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class, spec -> spec.supplier(context -> new Foo())); + } + } + + private static class TargetTypeBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("fooSupplierFromResolvableType", Foo.class, + spec -> spec.targetType(ResolvableType.forClassWithGenerics(Supplier.class, Foo.class))); + ParameterizedTypeReference> type = new ParameterizedTypeReference<>() {}; + registry.registerBean("fooSupplierFromTypeReference", Supplier.class, + spec -> spec.targetType(type)); + } + } + + private static class Foo {} + +} 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 4c709272428b..c97f88c61200 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. @@ -22,12 +22,11 @@ import java.util.stream.Stream; import org.assertj.core.api.ThrowableAssert.ThrowingCallable; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; - +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}. @@ -147,8 +146,7 @@ float getFloat() { static class MyReplacer implements MethodReplacer { - @Nullable - Object returnValue; + @Nullable Object returnValue; void reset() { this.returnValue = null; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java index 144bc37564fd..6c8b10ca7ca6 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/ConstructorResolverAotTests.java @@ -22,6 +22,7 @@ import java.util.Locale; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.FactoryBean; @@ -32,7 +33,6 @@ import org.springframework.beans.testfixture.beans.factory.generator.factory.SampleFactory; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java index f9ecb7e6c7d7..12d0fbdbf7e3 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/support/QualifierAnnotationAutowireBeanFactoryTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,13 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; +import org.springframework.beans.factory.annotation.QualifierAnnotationAutowireCandidateResolver; import org.springframework.beans.factory.config.ConstructorArgumentValues; import org.springframework.beans.factory.config.DependencyDescriptor; +import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.util.ClassUtils; @@ -43,14 +44,17 @@ class QualifierAnnotationAutowireBeanFactoryTests { private static final String MARK = "mark"; + private final DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + + @Test void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isTrue(); @@ -60,12 +64,12 @@ void testAutowireCandidateDefaultWithIrrelevantDescriptor() throws Exception { @Test void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition rbd = new RootBeanDefinition(Person.class, cavs, null); rbd.setAutowireCandidate(false); lbf.registerBeanDefinition(JUERGEN, rbd); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, new DependencyDescriptor(Person.class.getDeclaredField("name"), false))).isFalse(); @@ -73,44 +77,46 @@ void testAutowireCandidateExplicitlyFalseWithIrrelevantDescriptor() throws Excep new DependencyDescriptor(Person.class.getDeclaredField("name"), true))).isFalse(); } - @Disabled @Test void testAutowireCandidateWithFieldDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); + assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); - assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } @Test void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.setAutowireCandidate(false); person.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isFalse(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isFalse(); @@ -118,56 +124,61 @@ void testAutowireCandidateExplicitlyFalseWithFieldDescriptor() throws Exception @Test void testAutowireCandidateWithShortClassName() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs = new ConstructorArgumentValues(); cavs.addGenericArgumentValue(JUERGEN); RootBeanDefinition person = new RootBeanDefinition(Person.class, cavs, null); person.addQualifier(new AutowireCandidateQualifier(ClassUtils.getShortName(TestQualifier.class))); lbf.registerBeanDefinition(JUERGEN, person); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("qualified"), false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor( QualifiedTestBean.class.getDeclaredField("nonqualified"), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); } - @Disabled @Test void testAutowireCandidateWithConstructorDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + MethodParameter param = new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0); DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(param, false); param.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + assertThat(param.getParameterName()).isEqualTo("tpb"); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } - @Disabled @Test void testAutowireCandidateWithMethodDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); lbf.registerBeanDefinition(MARK, person2); + MethodParameter qualifiedParam = new MethodParameter(QualifiedTestBean.class.getDeclaredMethod("autowireQualified", Person.class), 0); MethodParameter nonqualifiedParam = @@ -175,37 +186,70 @@ void testAutowireCandidateWithMethodDescriptor() throws Exception { DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor(qualifiedParam, false); DependencyDescriptor nonqualifiedDescriptor = new DependencyDescriptor(nonqualifiedParam, false); qualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); - assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); nonqualifiedParam.initParameterNameDiscovery(new DefaultParameterNameDiscoverer()); + + assertThat(qualifiedParam.getParameterName()).isEqualTo("tpb"); assertThat(nonqualifiedParam.getParameterName()).isEqualTo("tpb"); - assertThat(lbf.isAutowireCandidate(JUERGEN, null)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); - assertThat(lbf.isAutowireCandidate(MARK, null)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, nonqualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isFalse(); } @Test void testAutowireCandidateWithMultipleCandidatesDescriptor() throws Exception { - DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(); ConstructorArgumentValues cavs1 = new ConstructorArgumentValues(); cavs1.addGenericArgumentValue(JUERGEN); RootBeanDefinition person1 = new RootBeanDefinition(Person.class, cavs1, null); person1.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(JUERGEN, person1); + ConstructorArgumentValues cavs2 = new ConstructorArgumentValues(); cavs2.addGenericArgumentValue(MARK); RootBeanDefinition person2 = new RootBeanDefinition(Person.class, cavs2, null); person2.addQualifier(new AutowireCandidateQualifier(TestQualifier.class)); lbf.registerBeanDefinition(MARK, person2); + DependencyDescriptor qualifiedDescriptor = new DependencyDescriptor( new MethodParameter(QualifiedTestBean.class.getDeclaredConstructor(Person.class), 0), false); + assertThat(lbf.isAutowireCandidate(JUERGEN, qualifiedDescriptor)).isTrue(); assertThat(lbf.isAutowireCandidate(MARK, qualifiedDescriptor)).isTrue(); } + @Test + void autowireBeanByTypeWithQualifierPrecedence() throws Exception { + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + lbf.registerBeanDefinition("testBean", bd); + lbf.registerBeanDefinition("spouse", bd2); + lbf.registerAlias("test", "testBean"); + + assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null)) + .isSameAs(lbf.getBean("spouse")); + } + + @Test + void autowireBeanByTypeWithQualifierPrecedenceInAncestor() throws Exception { + DefaultListableBeanFactory parent = new DefaultListableBeanFactory(); + parent.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + RootBeanDefinition bd = new RootBeanDefinition(TestBean.class); + RootBeanDefinition bd2 = new RootBeanDefinition(TestBean.class); + parent.registerBeanDefinition("test", bd); + parent.registerBeanDefinition("spouse", bd2); + parent.registerAlias("test", "testBean"); + + DefaultListableBeanFactory lbf = new DefaultListableBeanFactory(parent); + lbf.setAutowireCandidateResolver(new QualifierAnnotationAutowireCandidateResolver()); + + assertThat(lbf.resolveDependency(new DependencyDescriptor(getClass().getDeclaredField("testBean"), true), null)) + .isSameAs(lbf.getBean("spouse")); + } + @SuppressWarnings("unused") private static class QualifiedTestBean { @@ -247,4 +291,8 @@ public String getName() { private @interface TestQualifier { } + + @Qualifier("spouse") + private TestBean testBean; + } diff --git a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java index 5229dc46f434..3ca9366496f0 100644 --- a/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/factory/xml/ResourceEntityResolverTests.java @@ -16,13 +16,13 @@ package org.springframework.beans.factory.xml; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.xml.sax.InputSource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -94,8 +94,7 @@ private static class ConfigurableFallbackEntityResolver extends ResourceEntityRe private final boolean shouldThrow; - @Nullable - private final InputSource returnValue; + private final @Nullable InputSource returnValue; boolean fallbackInvoked = false; @@ -112,8 +111,7 @@ private ConfigurableFallbackEntityResolver(@Nullable InputSource returnValue) { } @Override - @Nullable - protected InputSource resolveSchemaEntity(String publicId, String systemId) { + protected @Nullable InputSource resolveSchemaEntity(String publicId, String systemId) { this.fallbackInvoked = true; if (this.shouldThrow) { throw new ResolutionRejectedException(); diff --git a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java index 110ea9979746..538bc6db4707 100644 --- a/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java +++ b/spring-beans/src/test/java/org/springframework/beans/support/PagedListHolderTests.java @@ -19,10 +19,10 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; 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 5b8378a49533..eb992aa5c73c 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/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt index b0d28ada8c2e..90b446c8a4bf 100644 --- a/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt +++ b/spring-beans/src/test/kotlin/org/springframework/beans/factory/aot/InstanceSupplierCodeGeneratorKotlinTests.kt @@ -56,8 +56,7 @@ class InstanceSupplierCodeGeneratorKotlinTests { Assertions.assertThat(bean).isInstanceOf(KotlinTestBean::class.java) Assertions.assertThat(compiled.sourceFile).contains("InstanceSupplier.using(KotlinTestBean::new)") } - Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java)) - .satisfies(hasConstructorWithMode(ExecutableMode.INTROSPECT)) + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinTestBean::class.java)).isNotNull } @Test @@ -90,8 +89,7 @@ class InstanceSupplierCodeGeneratorKotlinTests { "getBeanFactory().getBean(\"config\", KotlinConfiguration.class).stringBean()" ) } - Assertions.assertThat(getReflectionHints().getTypeHint(KotlinConfiguration::class.java)) - .satisfies(hasMethodWithMode(ExecutableMode.INTROSPECT)) + Assertions.assertThat(getReflectionHints().getTypeHint(KotlinConfiguration::class.java)).isNotNull } @Test diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java index 971ac2bdb31f..eced628a4567 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/NestedTestBean.java @@ -16,7 +16,7 @@ package org.springframework.beans.testfixture.beans; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple nested test bean used for testing bean factories, AOP framework etc. diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java index 343c5db3fdc9..4168a6751cef 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/Pet.java @@ -18,7 +18,7 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Rob Harrop diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java index 3ff88b183654..e27ed937ae64 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/SerializablePerson.java @@ -18,7 +18,8 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java index 580e117c3126..e50b9246aac0 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/TestBean.java @@ -27,10 +27,11 @@ import java.util.Properties; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.BeanNameAware; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java index 3d4288278c8b..e94ceaeaea22 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/DeferredTypeBuilder.java @@ -18,8 +18,9 @@ import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -31,8 +32,7 @@ */ public class DeferredTypeBuilder implements Consumer { - @Nullable - private Consumer type; + private @Nullable Consumer type; @Override public void accept(TypeSpec.Builder type) { diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java index 571b2d1509bc..693c9173b0d3 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/GenericFactoryBean.java @@ -16,9 +16,10 @@ package org.springframework.beans.testfixture.beans.factory.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; /** * A public {@link FactoryBean} with a generic type. @@ -33,15 +34,13 @@ public GenericFactoryBean(Class beanType) { this.beanType = beanType; } - @Nullable @Override - public T getObject() throws Exception { + public @Nullable T getObject() throws Exception { return BeanUtils.instantiateClass(this.beanType); } - @Nullable @Override - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.beanType; } } diff --git a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java index dc39666d1a57..ee25b9bb9b5f 100644 --- a/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java +++ b/spring-beans/src/testFixtures/java/org/springframework/beans/testfixture/beans/factory/aot/package-info.java @@ -1,9 +1,7 @@ /** * Test fixtures for bean factories AOT support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.beans.testfixture.beans.factory.aot; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 6f0b03c83eb2..042b1fe028fc 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 NoDependencyComponent() { public class EnvironmentAwareComponent { + final Environment environment; + public EnvironmentAwareComponent(Environment environment) { + this.environment = environment; } } @@ -40,7 +43,10 @@ public class NoDependencyComponentWithoutPublicConstructor { public class EnvironmentAwareComponentWithoutPublicConstructor { + final Environment environment; + EnvironmentAwareComponentWithoutPublicConstructor(Environment environment) { + this.environment = environment; } } diff --git a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java index c1442e32677a..7cae029a3730 100644 --- a/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java +++ b/spring-context-indexer/src/main/java/org/springframework/context/index/processor/MetadataCollector.java @@ -93,8 +93,8 @@ public CandidateComponentsMetadata getMetadata() { private boolean shouldBeMerged(ItemMetadata itemMetadata) { String sourceType = itemMetadata.getType(); - return (sourceType != null && !deletedInCurrentBuild(sourceType) - && !processedInCurrentBuild(sourceType)); + return (sourceType != null && !deletedInCurrentBuild(sourceType) && + !processedInCurrentBuild(sourceType)); } private boolean deletedInCurrentBuild(String sourceType) { diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java index 995081488379..531782f40430 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/CandidateComponentsIndexerTests.java @@ -21,7 +21,6 @@ import java.io.IOException; import java.nio.file.Path; -import jakarta.annotation.ManagedBean; import jakarta.inject.Named; import jakarta.persistence.Converter; import jakarta.persistence.Embeddable; @@ -43,7 +42,6 @@ import org.springframework.context.index.sample.SampleNone; import org.springframework.context.index.sample.SampleRepository; import org.springframework.context.index.sample.SampleService; -import org.springframework.context.index.sample.cdi.SampleManagedBean; import org.springframework.context.index.sample.cdi.SampleNamed; import org.springframework.context.index.sample.cdi.SampleTransactional; import org.springframework.context.index.sample.jpa.SampleConverter; @@ -126,11 +124,6 @@ void stereotypeOnAbstractClass() { testComponent(AbstractController.class); } - @Test - void cdiManagedBean() { - testSingleComponent(SampleManagedBean.class, ManagedBean.class); - } - @Test void cdiNamed() { testSingleComponent(SampleNamed.class, Named.class); diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java index 74c39cfedb7b..23ac0c64c5f0 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java +++ b/spring-context-indexer/src/test/java/org/springframework/context/index/processor/Metadata.java @@ -42,8 +42,8 @@ public static Condition of(String type, ItemMetadata itemMetadata = metadata.getItems().stream() .filter(item -> item.getType().equals(type)) .findFirst().orElse(null); - return itemMetadata != null && itemMetadata.getStereotypes().size() == stereotypes.size() - && itemMetadata.getStereotypes().containsAll(stereotypes); + return (itemMetadata != null && itemMetadata.getStereotypes().size() == stereotypes.size() && + itemMetadata.getStereotypes().containsAll(stereotypes)); }, "Candidates with type %s and stereotypes %s", type, stereotypes); } diff --git a/spring-context-support/spring-context-support.gradle b/spring-context-support/spring-context-support.gradle index a2f0c083e734..851125b73e56 100644 --- a/spring-context-support/spring-context-support.gradle +++ b/spring-context-support/spring-context-support.gradle @@ -23,7 +23,7 @@ dependencies { testImplementation("io.projectreactor:reactor-core") testImplementation("jakarta.annotation:jakarta.annotation-api") testImplementation("org.hsqldb:hsqldb") - testRuntimeOnly("com.sun.mail:jakarta.mail") + testRuntimeOnly("org.eclipse.angus:angus-mail") testRuntimeOnly("org.ehcache:ehcache") testRuntimeOnly("org.ehcache:jcache") testRuntimeOnly("org.glassfish:jakarta.el") diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java index da4b62e471c6..f5ffec852a59 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCache.java @@ -23,9 +23,9 @@ import com.github.benmanes.caffeine.cache.AsyncCache; import com.github.benmanes.caffeine.cache.LoadingCache; +import org.jspecify.annotations.Nullable; import org.springframework.cache.support.AbstractValueAdaptingCache; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -50,8 +50,7 @@ public class CaffeineCache extends AbstractValueAdaptingCache { private final com.github.benmanes.caffeine.cache.Cache cache; - @Nullable - private AsyncCache asyncCache; + private @Nullable AsyncCache asyncCache; /** @@ -130,14 +129,12 @@ public final AsyncCache getAsyncCache() { @SuppressWarnings("unchecked") @Override - @Nullable - public T get(Object key, Callable valueLoader) { + public @Nullable T get(Object key, Callable valueLoader) { return (T) fromStoreValue(this.cache.get(key, new LoadFunction(valueLoader))); } @Override - @Nullable - public CompletableFuture retrieve(Object key) { + public @Nullable CompletableFuture retrieve(Object key) { CompletableFuture result = getAsyncCache().getIfPresent(key); if (result != null && isAllowNullValues()) { result = result.thenApply(this::toValueWrapper); @@ -159,8 +156,7 @@ public CompletableFuture retrieve(Object key, Supplier loadingCache) { return loadingCache.get(key); } @@ -173,8 +169,7 @@ public void put(Object key, @Nullable Object value) { } @Override - @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + public @Nullable ValueWrapper putIfAbsent(Object key, @Nullable Object value) { PutIfAbsentFunction callable = new PutIfAbsentFunction(value); Object result = this.cache.get(key, callable); return (callable.called ? null : toValueWrapper(result)); @@ -205,8 +200,7 @@ public boolean invalidate() { private class PutIfAbsentFunction implements Function { - @Nullable - private final Object value; + private final @Nullable Object value; boolean called; diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java index 2ad316114bdc..b08f9d376ee4 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/CaffeineCacheManager.java @@ -29,10 +29,10 @@ import com.github.benmanes.caffeine.cache.CacheLoader; import com.github.benmanes.caffeine.cache.Caffeine; import com.github.benmanes.caffeine.cache.CaffeineSpec; +import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -70,8 +70,7 @@ public class CaffeineCacheManager implements CacheManager { private Caffeine cacheBuilder = Caffeine.newBuilder(); - @Nullable - private AsyncCacheLoader cacheLoader; + private @Nullable AsyncCacheLoader cacheLoader; private boolean asyncCacheMode = false; @@ -251,8 +250,7 @@ public Collection getCacheNames() { } @Override - @Nullable - public Cache getCache(String name) { + public @Nullable Cache getCache(String name) { Cache cache = this.cacheMap.get(name); if (cache == null && this.dynamic) { cache = this.cacheMap.computeIfAbsent(name, this::createCaffeineCache); diff --git a/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java index 864fb2e3997d..5606a4fdffec 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/cache/caffeine/package-info.java @@ -3,9 +3,7 @@ * Caffeine library, * allowing to set up Caffeine caches within Spring's cache abstraction. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.caffeine; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java index c870d843d736..a465275f4a47 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.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. @@ -16,6 +16,7 @@ package org.springframework.cache.jcache; +import java.util.Objects; import java.util.concurrent.Callable; import java.util.function.Function; @@ -24,8 +25,9 @@ import javax.cache.processor.EntryProcessorException; import javax.cache.processor.MutableEntry; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.support.AbstractValueAdaptingCache; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -79,15 +81,13 @@ public final Cache getNativeCache() { } @Override - @Nullable - protected Object lookup(Object key) { + protected @Nullable Object lookup(Object key) { return this.cache.get(key); } @Override - @Nullable @SuppressWarnings("unchecked") - public T get(Object key, Callable valueLoader) { + public @Nullable T get(Object key, Callable valueLoader) { try { return (T) this.cache.invoke(key, this.valueLoaderEntryProcessor, valueLoader); } @@ -102,8 +102,7 @@ public void put(Object key, @Nullable Object value) { } @Override - @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + public @Nullable ValueWrapper putIfAbsent(Object key, @Nullable Object value) { Object previous = this.cache.invoke(key, PutIfAbsentEntryProcessor.INSTANCE, toStoreValue(value)); return (previous != null ? toValueWrapper(previous) : null); } @@ -136,8 +135,8 @@ private static class PutIfAbsentEntryProcessor implements EntryProcessor entry, Object... arguments) throws EntryProcessorException { + @SuppressWarnings("NullAway") // Overridden method does not define nullness + public @Nullable Object process(MutableEntry entry, @Nullable Object... arguments) throws EntryProcessorException { Object existingValue = entry.getValue(); if (existingValue == null) { entry.setValue(arguments[0]); @@ -149,11 +148,11 @@ public Object process(MutableEntry entry, Object... arguments) t private static final class ValueLoaderEntryProcessor implements EntryProcessor { - private final Function fromStoreValue; + private final Function fromStoreValue; private final Function toStoreValue; - private ValueLoaderEntryProcessor(Function fromStoreValue, + private ValueLoaderEntryProcessor(Function fromStoreValue, Function toStoreValue) { this.fromStoreValue = fromStoreValue; @@ -161,17 +160,16 @@ private ValueLoaderEntryProcessor(Function fromStoreValue, } @Override - @Nullable - @SuppressWarnings("unchecked") - public Object process(MutableEntry entry, Object... arguments) throws EntryProcessorException { + @SuppressWarnings({"unchecked","NullAway"}) // Overridden method does not define nullness + public @Nullable Object process(MutableEntry entry, @Nullable Object... arguments) throws EntryProcessorException { Callable valueLoader = (Callable) arguments[0]; if (entry.exists()) { - return this.fromStoreValue.apply(entry.getValue()); + return this.fromStoreValue.apply(Objects.requireNonNull(entry.getValue())); } else { Object value; try { - value = valueLoader.call(); + value = Objects.requireNonNull(valueLoader).call(); } catch (Exception ex) { throw new EntryProcessorException("Value loader '" + valueLoader + "' failed " + diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java index 0e87c8af53ad..c2e90d6e9715 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCacheManager.java @@ -22,9 +22,10 @@ import javax.cache.CacheManager; import javax.cache.Caching; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -40,8 +41,7 @@ */ public class JCacheCacheManager extends AbstractTransactionSupportingCacheManager { - @Nullable - private CacheManager cacheManager; + private @Nullable CacheManager cacheManager; private boolean allowNullValues = true; @@ -75,8 +75,7 @@ public void setCacheManager(@Nullable CacheManager cacheManager) { /** * Return the backing JCache {@link CacheManager javax.cache.CacheManager}. */ - @Nullable - public CacheManager getCacheManager() { + public @Nullable CacheManager getCacheManager() { return this.cacheManager; } @@ -121,8 +120,7 @@ protected Collection loadCaches() { } @Override - @Nullable - protected Cache getMissingCache(String name) { + protected @Nullable Cache getMissingCache(String name) { CacheManager cacheManager = getCacheManager(); Assert.state(cacheManager != null, "No CacheManager set"); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java index da3a2e15669d..29d246a09f42 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheManagerFactoryBean.java @@ -22,11 +22,12 @@ import javax.cache.CacheManager; import javax.cache.Caching; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} for a JCache {@link CacheManager javax.cache.CacheManager}, @@ -43,17 +44,13 @@ public class JCacheManagerFactoryBean implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean { - @Nullable - private URI cacheManagerUri; + private @Nullable URI cacheManagerUri; - @Nullable - private Properties cacheManagerProperties; + private @Nullable Properties cacheManagerProperties; - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; - @Nullable - private CacheManager cacheManager; + private @Nullable CacheManager cacheManager; /** @@ -86,8 +83,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public CacheManager getObject() { + public @Nullable CacheManager getObject() { return this.cacheManager; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java index 793435d03f46..6237426b73fa 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.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 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.cache.annotation.AbstractCachingConfiguration; import org.springframework.cache.interceptor.CacheResolver; @@ -26,7 +28,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Role; -import org.springframework.lang.Nullable; /** * Abstract JSR-107 specific {@code @Configuration} class providing common @@ -40,11 +41,11 @@ @Configuration(proxyBeanMethods = false) public abstract class AbstractJCacheConfiguration extends AbstractCachingConfiguration { - @Nullable - protected Supplier exceptionCacheResolver; + protected @Nullable Supplier<@Nullable CacheResolver> exceptionCacheResolver; @Override + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) { super.useCachingConfigurer(cachingConfigurerSupplier); this.exceptionCacheResolver = cachingConfigurerSupplier.adapt(config -> { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java index 2690c1ac37bd..d834cd7c35a6 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java @@ -16,9 +16,10 @@ package org.springframework.cache.jcache.config; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.annotation.CachingConfigurer; import org.springframework.cache.interceptor.CacheResolver; -import org.springframework.lang.Nullable; /** * Extension of {@link CachingConfigurer} for the JSR-107 implementation. @@ -57,8 +58,7 @@ public interface JCacheConfigurer extends CachingConfigurer { * * See {@link org.springframework.cache.annotation.EnableCaching} for more complete examples. */ - @Nullable - default CacheResolver exceptionCacheResolver() { + default @Nullable CacheResolver exceptionCacheResolver() { return null; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java index 76f4b4570266..6f991f5f7316 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurerSupport.java @@ -16,9 +16,10 @@ package org.springframework.cache.jcache.config; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.interceptor.CacheResolver; -import org.springframework.lang.Nullable; /** * An extension of {@link CachingConfigurerSupport} that also implements @@ -37,8 +38,7 @@ public class JCacheConfigurerSupport extends CachingConfigurerSupport implements JCacheConfigurer { @Override - @Nullable - public CacheResolver exceptionCacheResolver() { + public @Nullable CacheResolver exceptionCacheResolver() { return null; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java index c5adcac5ac60..46899ffad14c 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java @@ -6,9 +6,7 @@ *

Provides an extension of the {@code CachingConfigurer} that exposes * the exception cache resolver to use (see {@code JCacheConfigurer}). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.jcache.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java index 06025fc26a13..e128f2f3662e 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java @@ -22,13 +22,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; import org.springframework.cache.interceptor.AbstractCacheInvoker; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -51,8 +51,7 @@ protected AbstractCacheInterceptor(CacheErrorHandler errorHandler) { } - @Nullable - protected abstract Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) + protected abstract @Nullable Object invoke(CacheOperationInvocationContext context, CacheOperationInvoker invoker) throws Throwable; @@ -75,8 +74,7 @@ protected Cache resolveCache(CacheOperationInvocationContext context) { *

Throw an {@link IllegalStateException} if the collection holds more than one element * @return the single element, or {@code null} if the collection is empty */ - @Nullable - static Cache extractFrom(Collection caches) { + static @Nullable Cache extractFrom(Collection caches) { if (CollectionUtils.isEmpty(caches)) { return null; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java index 0ff896eb5e36..a96640275822 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -23,10 +23,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -59,13 +59,11 @@ public boolean hasCacheOperation(Method method, @Nullable Class targetClass) } @Override - @Nullable - public JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { + public @Nullable JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass) { return getCacheOperation(method, targetClass, true); } - @Nullable - private JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass, boolean cacheNull) { + private @Nullable JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass, boolean cacheNull) { if (ReflectionUtils.isObjectMethod(method)) { return null; } @@ -91,8 +89,7 @@ else if (cacheNull) { } } - @Nullable - private JCacheOperation computeCacheOperation(Method method, @Nullable Class targetClass) { + private @Nullable JCacheOperation computeCacheOperation(Method method, @Nullable Class targetClass) { // Don't allow non-public methods, as configured. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; @@ -126,8 +123,7 @@ private JCacheOperation computeCacheOperation(Method method, @Nullable Class< * @return the cache operation associated with this method * (or {@code null} if none) */ - @Nullable - protected abstract JCacheOperation findCacheOperation(Method method, @Nullable Class targetType); + protected abstract @Nullable JCacheOperation findCacheOperation(Method method, @Nullable Class targetType); /** * Should only public methods be allowed to have caching semantics? diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java index e3ecd6fa3b25..9dcf4ad4954a 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheKeyOperation.java @@ -23,6 +23,8 @@ import javax.cache.annotation.CacheInvocationParameter; import javax.cache.annotation.CacheMethodDetails; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; @@ -74,13 +76,13 @@ public KeyGenerator getKeyGenerator() { * @return the {@link CacheInvocationParameter} instances for the parameters to be * used to compute the key */ - public CacheInvocationParameter[] getKeyParameters(Object... values) { + public CacheInvocationParameter[] getKeyParameters(@Nullable Object... values) { List result = new ArrayList<>(); for (CacheParameterDetail keyParameterDetail : this.keyParameterDetails) { int parameterPosition = keyParameterDetail.getParameterPosition(); if (parameterPosition >= values.length) { - throw new IllegalStateException("Values mismatch, key parameter at position " - + parameterPosition + " cannot be matched against " + values.length + " value(s)"); + throw new IllegalStateException("Values mismatch, key parameter at position " + + parameterPosition + " cannot be matched against " + values.length + " value(s)"); } result.add(keyParameterDetail.toCacheInvocationParameter(values[parameterPosition])); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java index 036a4f6cb587..fdf8b27749fb 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractJCacheOperation.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -30,6 +30,8 @@ import javax.cache.annotation.CacheMethodDetails; import javax.cache.annotation.CacheValue; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheResolver; import org.springframework.util.Assert; import org.springframework.util.ExceptionTypeFilter; @@ -105,7 +107,7 @@ public CacheResolver getCacheResolver() { } @Override - public CacheInvocationParameter[] getAllParameters(Object... values) { + public CacheInvocationParameter[] getAllParameters(@Nullable Object... values) { if (this.allParameterDetails.size() != values.length) { throw new IllegalStateException("Values mismatch, operation has " + this.allParameterDetails.size() + " parameter(s) but got " + values.length + " value(s)"); @@ -200,7 +202,7 @@ protected boolean isValue() { return this.isValue; } - public CacheInvocationParameter toCacheInvocationParameter(Object value) { + public CacheInvocationParameter toCacheInvocationParameter(@Nullable Object value) { return new CacheInvocationParameterImpl(this, value); } } @@ -213,9 +215,9 @@ protected static class CacheInvocationParameterImpl implements CacheInvocationPa private final CacheParameterDetail detail; - private final Object value; + private final @Nullable Object value; - public CacheInvocationParameterImpl(CacheParameterDetail detail, Object value) { + public CacheInvocationParameterImpl(CacheParameterDetail detail, @Nullable Object value) { this.detail = detail; this.value = value; } @@ -226,7 +228,7 @@ public Class getRawType() { } @Override - public Object getValue() { + public @Nullable Object getValue() { return this.value; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java index e18941574a11..77a989aa1050 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationJCacheOperationSource.java @@ -31,10 +31,11 @@ import javax.cache.annotation.CacheResolverFactory; import javax.cache.annotation.CacheResult; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -58,8 +59,7 @@ public boolean isCandidateClass(Class targetClass) { } @Override - @Nullable - protected JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { + protected @Nullable JCacheOperation findCacheOperation(Method method, @Nullable Class targetType) { CacheResult cacheResult = method.getAnnotation(CacheResult.class); CachePut cachePut = method.getAnnotation(CachePut.class); CacheRemove cacheRemove = method.getAnnotation(CacheRemove.class); @@ -88,8 +88,7 @@ else if (cacheRemove != null) { } } - @Nullable - protected CacheDefaults getCacheDefaults(Method method, @Nullable Class targetType) { + protected @Nullable CacheDefaults getCacheDefaults(Method method, @Nullable Class targetType) { CacheDefaults annotation = method.getDeclaringClass().getAnnotation(CacheDefaults.class); if (annotation != null) { return annotation; @@ -175,8 +174,7 @@ protected CacheResolver getExceptionCacheResolver( } } - @Nullable - protected CacheResolverFactory determineCacheResolverFactory( + protected @Nullable CacheResolverFactory determineCacheResolverFactory( @Nullable CacheDefaults defaults, Class candidate) { if (candidate != CacheResolverFactory.class) { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java index f64c09a336f1..61dcdfc07154 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java @@ -19,11 +19,12 @@ import javax.cache.annotation.CacheKeyInvocationContext; import javax.cache.annotation.CachePut; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; -import org.springframework.lang.Nullable; /** * Intercept methods annotated with {@link CachePut}. @@ -40,8 +41,7 @@ public CachePutInterceptor(CacheErrorHandler errorHandler) { @Override - @Nullable - protected Object invoke( + protected @Nullable Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { CachePutOperation operation = context.getOperation(); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java index a406418863f2..d35ddfe71557 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutOperation.java @@ -23,9 +23,10 @@ import javax.cache.annotation.CacheMethodDetails; import javax.cache.annotation.CachePut; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.lang.Nullable; import org.springframework.util.ExceptionTypeFilter; /** @@ -81,7 +82,7 @@ public boolean isEarlyPut() { * @param values the parameters value for a particular invocation * @return the {@link CacheInvocationParameter} instance for the value parameter */ - public CacheInvocationParameter getValueParameter(Object... values) { + public CacheInvocationParameter getValueParameter(@Nullable Object... values) { int parameterPosition = this.valueParameterDetail.getParameterPosition(); if (parameterPosition >= values.length) { throw new IllegalStateException("Values mismatch, value parameter at position " + @@ -91,8 +92,7 @@ public CacheInvocationParameter getValueParameter(Object... values) { } - @Nullable - private static CacheParameterDetail initializeValueParameterDetail( + private static @Nullable CacheParameterDetail initializeValueParameterDetail( Method method, List allParameters) { CacheParameterDetail result = null; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java index dfd8f0a4cc0e..557cc3fd6c28 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java @@ -18,11 +18,12 @@ import javax.cache.annotation.CacheRemoveAll; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; -import org.springframework.lang.Nullable; /** * Intercept methods annotated with {@link CacheRemoveAll}. @@ -39,8 +40,7 @@ protected CacheRemoveAllInterceptor(CacheErrorHandler errorHandler) { @Override - @Nullable - protected Object invoke( + protected @Nullable Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { CacheRemoveAllOperation operation = context.getOperation(); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java index a1075785bb24..95ac57f666c7 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java @@ -18,11 +18,12 @@ import javax.cache.annotation.CacheRemove; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; -import org.springframework.lang.Nullable; /** * Intercept methods annotated with {@link CacheRemove}. @@ -39,8 +40,7 @@ protected CacheRemoveEntryInterceptor(CacheErrorHandler errorHandler) { @Override - @Nullable - protected Object invoke( + protected @Nullable Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { CacheRemoveOperation operation = context.getOperation(); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java index dbd71ba26485..f071be030c9e 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java @@ -18,12 +18,13 @@ import javax.cache.annotation.CacheResult; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.interceptor.CacheResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ExceptionTypeFilter; import org.springframework.util.SerializationUtils; @@ -43,8 +44,7 @@ public CacheResultInterceptor(CacheErrorHandler errorHandler) { @Override - @Nullable - protected Object invoke( + protected @Nullable Object invoke( CacheOperationInvocationContext context, CacheOperationInvoker invoker) { CacheResultOperation operation = context.getOperation(); @@ -97,8 +97,7 @@ protected void cacheException(@Nullable Cache exceptionCache, ExceptionTypeFilte } } - @Nullable - private Cache resolveExceptionCache(CacheOperationInvocationContext context) { + private @Nullable Cache resolveExceptionCache(CacheOperationInvocationContext context) { CacheResolver exceptionCacheResolver = context.getOperation().getExceptionCacheResolver(); if (exceptionCacheResolver != null) { return extractFrom(exceptionCacheResolver.resolveCaches(context)); @@ -146,8 +145,7 @@ private static CacheOperationInvoker.ThrowableWrapper rewriteCallStack( return new CacheOperationInvoker.ThrowableWrapper(clone); } - @Nullable - private static T cloneException(T exception) { + private static @Nullable T cloneException(T exception) { try { return SerializationUtils.clone(exception); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java index 3f4eedfe1020..7435599a7cef 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultOperation.java @@ -19,9 +19,10 @@ import javax.cache.annotation.CacheMethodDetails; import javax.cache.annotation.CacheResult; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.lang.Nullable; import org.springframework.util.ExceptionTypeFilter; import org.springframework.util.StringUtils; @@ -36,11 +37,9 @@ class CacheResultOperation extends AbstractJCacheKeyOperation { private final ExceptionTypeFilter exceptionTypeFilter; - @Nullable - private final CacheResolver exceptionCacheResolver; + private final @Nullable CacheResolver exceptionCacheResolver; - @Nullable - private final String exceptionCacheName; + private final @Nullable String exceptionCacheName; public CacheResultOperation(CacheMethodDetails methodDetails, CacheResolver cacheResolver, @@ -73,8 +72,7 @@ public boolean isAlwaysInvoked() { * Return the {@link CacheResolver} instance to use to resolve the cache to * use for matching exceptions thrown by this operation. */ - @Nullable - public CacheResolver getExceptionCacheResolver() { + public @Nullable CacheResolver getExceptionCacheResolver() { return this.exceptionCacheResolver; } @@ -83,8 +81,7 @@ public CacheResolver getExceptionCacheResolver() { * caching exceptions should be disabled. * @see javax.cache.annotation.CacheResult#exceptionCacheName() */ - @Nullable - public String getExceptionCacheName() { + public @Nullable String getExceptionCacheName() { return this.exceptionCacheName; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java index 35523c2b46a4..8509635beca2 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java @@ -24,6 +24,8 @@ import javax.cache.annotation.CacheInvocationContext; import javax.cache.annotation.CacheInvocationParameter; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheOperationInvocationContext; /** @@ -42,12 +44,12 @@ class DefaultCacheInvocationContext private final Object target; - private final Object[] args; + private final @Nullable Object[] args; private final CacheInvocationParameter[] allParameters; - public DefaultCacheInvocationContext(JCacheOperation operation, Object target, Object[] args) { + public DefaultCacheInvocationContext(JCacheOperation operation, Object target, @Nullable Object[] args) { this.operation = operation; this.target = target; this.args = args; @@ -66,7 +68,7 @@ public Method getMethod() { } @Override - public Object[] getArgs() { + public @Nullable Object[] getArgs() { return this.args.clone(); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java index 9bd230ab9373..4e6a4a4915b9 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java @@ -21,7 +21,7 @@ import javax.cache.annotation.CacheInvocationParameter; import javax.cache.annotation.CacheKeyInvocationContext; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * The default {@link CacheKeyInvocationContext} implementation. @@ -35,11 +35,10 @@ class DefaultCacheKeyInvocationContext extends DefaultCach private final CacheInvocationParameter[] keyParameters; - @Nullable - private final CacheInvocationParameter valueParameter; + private final @Nullable CacheInvocationParameter valueParameter; - public DefaultCacheKeyInvocationContext(AbstractJCacheKeyOperation operation, Object target, Object[] args) { + public DefaultCacheKeyInvocationContext(AbstractJCacheKeyOperation operation, Object target, @Nullable Object[] args) { super(operation, target, args); this.keyParameters = operation.getKeyParameters(args); if (operation instanceof CachePutOperation cachePutOperation) { @@ -57,8 +56,7 @@ public CacheInvocationParameter[] getKeyParameters() { } @Override - @Nullable - public CacheInvocationParameter getValueParameter() { + public @Nullable CacheInvocationParameter getValueParameter() { return this.valueParameter; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java index 1b93b140ba2b..ed2a14e315c5 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.util.Collection; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -32,7 +34,6 @@ import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.cache.interceptor.SimpleCacheResolver; import org.springframework.cache.interceptor.SimpleKeyGenerator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; import org.springframework.util.function.SupplierUtils; @@ -49,22 +50,18 @@ public class DefaultJCacheOperationSource extends AnnotationJCacheOperationSource implements BeanFactoryAware, SmartInitializingSingleton { - @Nullable - private SingletonSupplier cacheManager; + private @Nullable SingletonSupplier cacheManager; - @Nullable - private SingletonSupplier cacheResolver; + private @Nullable SingletonSupplier cacheResolver; - @Nullable - private SingletonSupplier exceptionCacheResolver; + private @Nullable SingletonSupplier exceptionCacheResolver; private SingletonSupplier keyGenerator; private final SingletonSupplier adaptedKeyGenerator = SingletonSupplier.of(() -> new KeyGeneratorAdapter(this, getKeyGenerator())); - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** @@ -82,8 +79,8 @@ public DefaultJCacheOperationSource() { * @since 5.1 */ public DefaultJCacheOperationSource( - @Nullable Supplier cacheManager, @Nullable Supplier cacheResolver, - @Nullable Supplier exceptionCacheResolver, @Nullable Supplier keyGenerator) { + @Nullable Supplier<@Nullable CacheManager> cacheManager, @Nullable Supplier<@Nullable CacheResolver> cacheResolver, + @Nullable Supplier<@Nullable CacheResolver> exceptionCacheResolver, @Nullable Supplier<@Nullable KeyGenerator> keyGenerator) { this.cacheManager = SingletonSupplier.ofNullable(cacheManager); this.cacheResolver = SingletonSupplier.ofNullable(cacheResolver); @@ -103,8 +100,7 @@ public void setCacheManager(@Nullable CacheManager cacheManager) { /** * Return the specified cache manager to use, if any. */ - @Nullable - public CacheManager getCacheManager() { + public @Nullable CacheManager getCacheManager() { return SupplierUtils.resolve(this.cacheManager); } @@ -119,8 +115,7 @@ public void setCacheResolver(@Nullable CacheResolver cacheResolver) { /** * Return the specified cache resolver to use, if any. */ - @Nullable - public CacheResolver getCacheResolver() { + public @Nullable CacheResolver getCacheResolver() { return SupplierUtils.resolve(this.cacheResolver); } @@ -135,8 +130,7 @@ public void setExceptionCacheResolver(@Nullable CacheResolver exceptionCacheReso /** * Return the specified exception cache resolver to use, if any. */ - @Nullable - public CacheResolver getExceptionCacheResolver() { + public @Nullable CacheResolver getExceptionCacheResolver() { return SupplierUtils.resolve(this.exceptionCacheResolver); } @@ -188,7 +182,7 @@ protected T getBean(Class type) { } } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected CacheManager getDefaultCacheManager() { if (getCacheManager() == null) { Assert.state(this.beanFactory != null, "BeanFactory required for default CacheManager resolution"); @@ -208,7 +202,7 @@ protected CacheManager getDefaultCacheManager() { } @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected CacheResolver getDefaultCacheResolver() { if (getCacheResolver() == null) { this.cacheResolver = SingletonSupplier.of(new SimpleCacheResolver(getDefaultCacheManager())); @@ -217,7 +211,7 @@ protected CacheResolver getDefaultCacheResolver() { } @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected CacheResolver getDefaultExceptionCacheResolver() { if (getExceptionCacheResolver() == null) { this.exceptionCacheResolver = SingletonSupplier.of(new LazyCacheResolver()); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java index de1ff1a293af..68ae8c8d3d86 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.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. @@ -21,6 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.InitializingBean; @@ -28,7 +29,6 @@ import org.springframework.cache.interceptor.BasicOperation; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheOperationInvoker; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -53,20 +53,15 @@ public class JCacheAspectSupport extends AbstractCacheInvoker implements Initial protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private JCacheOperationSource cacheOperationSource; + private @Nullable JCacheOperationSource cacheOperationSource; - @Nullable - private CacheResultInterceptor cacheResultInterceptor; + private @Nullable CacheResultInterceptor cacheResultInterceptor; - @Nullable - private CachePutInterceptor cachePutInterceptor; + private @Nullable CachePutInterceptor cachePutInterceptor; - @Nullable - private CacheRemoveEntryInterceptor cacheRemoveEntryInterceptor; + private @Nullable CacheRemoveEntryInterceptor cacheRemoveEntryInterceptor; - @Nullable - private CacheRemoveAllInterceptor cacheRemoveAllInterceptor; + private @Nullable CacheRemoveAllInterceptor cacheRemoveAllInterceptor; private boolean initialized = false; @@ -101,8 +96,7 @@ public void afterPropertiesSet() { } - @Nullable - protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { + protected @Nullable Object execute(CacheOperationInvoker invoker, Object target, Method method, @Nullable Object[] args) { // Check whether aspect is enabled to cope with cases where the AJ is pulled in automatically if (this.initialized) { Class targetClass = AopProxyUtils.ultimateTargetClass(target); @@ -119,15 +113,14 @@ protected Object execute(CacheOperationInvoker invoker, Object target, Method me @SuppressWarnings("unchecked") private CacheOperationInvocationContext createCacheOperationInvocationContext( - Object target, Object[] args, JCacheOperation operation) { + Object target, @Nullable Object[] args, JCacheOperation operation) { return new DefaultCacheInvocationContext<>( (JCacheOperation) operation, target, args); } @SuppressWarnings("unchecked") - @Nullable - private Object execute(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { + private @Nullable Object execute(CacheOperationInvocationContext context, CacheOperationInvoker invoker) { CacheOperationInvoker adapter = new CacheOperationInvokerAdapter(invoker); BasicOperation operation = context.getOperation(); @@ -165,8 +158,7 @@ else if (operation instanceof CacheRemoveAllOperation) { * @return the result of the invocation * @see CacheOperationInvoker#invoke() */ - @Nullable - protected Object invokeOperation(CacheOperationInvoker invoker) { + protected @Nullable Object invokeOperation(CacheOperationInvoker invoker) { return invoker.invoke(); } @@ -180,8 +172,7 @@ public CacheOperationInvokerAdapter(CacheOperationInvoker delegate) { } @Override - @Nullable - public Object invoke() throws ThrowableWrapper { + public @Nullable Object invoke() throws ThrowableWrapper { return invokeOperation(this.delegate); } } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java index 81e65d1a1c1c..67aa162170d6 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.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. @@ -22,11 +22,11 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.interceptor.SimpleCacheErrorHandler; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; @@ -60,14 +60,13 @@ public JCacheInterceptor() { * applying the default error handler if the supplier is not resolvable * @since 5.1 */ - public JCacheInterceptor(@Nullable Supplier errorHandler) { + public JCacheInterceptor(@Nullable Supplier errorHandler) { this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new); } @Override - @Nullable - public Object invoke(final MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); CacheOperationInvoker aopAllianceInvoker = () -> { diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java index 00c5ef91e9cf..9781d4016aaa 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperation.java @@ -21,6 +21,8 @@ import javax.cache.annotation.CacheInvocationParameter; import javax.cache.annotation.CacheMethodDetails; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.BasicOperation; import org.springframework.cache.interceptor.CacheResolver; @@ -48,6 +50,6 @@ public interface JCacheOperation extends BasicOperation, C *

The method arguments must match the signature of the related method invocation * @param values the parameters value for a particular invocation */ - CacheInvocationParameter[] getAllParameters(Object... values); + CacheInvocationParameter[] getAllParameters(@Nullable Object... values); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java index 686066c6028f..9cdf04c93f16 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -18,7 +18,7 @@ import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface used by {@link JCacheInterceptor}. Implementations know how to source @@ -70,7 +70,6 @@ default boolean hasCacheOperation(Method method, @Nullable Class targetClass) * the declaring class of the method must be used) * @return the cache operation for this method, or {@code null} if none found */ - @Nullable - JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass); + @Nullable JCacheOperation getCacheOperation(Method method, @Nullable Class targetClass); } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java index bfd4bda19d9b..7f41c18767e2 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -19,10 +19,11 @@ import java.io.Serializable; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -35,8 +36,7 @@ @SuppressWarnings("serial") final class JCacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - @Nullable - private JCacheOperationSource cacheOperationSource; + private @Nullable JCacheOperationSource cacheOperationSource; public JCacheOperationSourcePointcut() { @@ -85,8 +85,7 @@ public boolean matches(Class clazz) { return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } - @Nullable - private JCacheOperationSource getCacheOperationSource() { + private @Nullable JCacheOperationSource getCacheOperationSource() { return cacheOperationSource; } diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java index 3fcfe9fc58d9..34ea80c85079 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,8 +25,9 @@ import javax.cache.annotation.CacheKeyGenerator; import javax.cache.annotation.CacheKeyInvocationContext; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -43,11 +44,9 @@ class KeyGeneratorAdapter implements KeyGenerator { private final JCacheOperationSource cacheOperationSource; - @Nullable - private KeyGenerator keyGenerator; + private @Nullable KeyGenerator keyGenerator; - @Nullable - private CacheKeyGenerator cacheKeyGenerator; + private @Nullable CacheKeyGenerator cacheKeyGenerator; /** @@ -85,7 +84,7 @@ public Object getTarget() { } @Override - public Object generate(Object target, Method method, Object... params) { + public Object generate(Object target, Method method, @Nullable Object... params) { JCacheOperation operation = this.cacheOperationSource.getCacheOperation(method, target.getClass()); if (!(operation instanceof AbstractJCacheKeyOperation)) { throw new IllegalStateException("Invalid operation, should be a key-based operation " + operation); @@ -119,7 +118,7 @@ private static Object doGenerate(KeyGenerator keyGenerator, CacheKeyInvocationCo @SuppressWarnings("unchecked") private CacheKeyInvocationContext createCacheKeyInvocationContext( - Object target, JCacheOperation operation, Object[] params) { + Object target, JCacheOperation operation, @Nullable Object[] params) { AbstractJCacheKeyOperation keyCacheOperation = (AbstractJCacheKeyOperation) operation; return new DefaultCacheKeyInvocationContext<>(keyCacheOperation, target, params); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java index 70eca3aad5e3..b35ff8dbd0db 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java @@ -19,12 +19,13 @@ import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.AbstractCacheResolver; import org.springframework.cache.interceptor.BasicOperation; import org.springframework.cache.interceptor.CacheOperationInvocationContext; import org.springframework.cache.interceptor.CacheResolver; -import org.springframework.lang.Nullable; /** * A simple {@link CacheResolver} that resolves the exception cache @@ -42,8 +43,7 @@ public SimpleExceptionCacheResolver(CacheManager cacheManager) { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { BasicOperation operation = context.getOperation(); if (!(operation instanceof CacheResultOperation cacheResultOperation)) { throw new IllegalStateException("Could not extract exception cache name from " + operation); diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java index c752c806b9fb..d834a0736552 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java @@ -7,9 +7,7 @@ *

Builds on the AOP infrastructure in org.springframework.aop.framework. * Any POJO can be cache-advised with Spring. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.jcache.interceptor; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java index e8b5c89807a2..568feb8512f0 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/package-info.java @@ -4,9 +4,7 @@ * and {@link org.springframework.cache.Cache Cache} implementation for * use in a Spring context, using a JSR-107 compliant cache provider. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.jcache; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java index 45b5870dfecc..107529bf1ade 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java +++ b/spring-context-support/src/main/java/org/springframework/cache/transaction/TransactionAwareCacheDecorator.java @@ -20,8 +20,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -77,26 +78,22 @@ public Object getNativeCache() { } @Override - @Nullable - public ValueWrapper get(Object key) { + public @Nullable ValueWrapper get(Object key) { return this.targetCache.get(key); } @Override - @Nullable - public T get(Object key, @Nullable Class type) { + public @Nullable T get(Object key, @Nullable Class type) { return this.targetCache.get(key, type); } @Override - @Nullable - public T get(Object key, Callable valueLoader) { + public @Nullable T get(Object key, Callable valueLoader) { return this.targetCache.get(key, valueLoader); } @Override - @Nullable - public CompletableFuture retrieve(Object key) { + public @Nullable CompletableFuture retrieve(Object key) { return this.targetCache.retrieve(key); } @@ -106,7 +103,7 @@ public CompletableFuture retrieve(Object key, Supplier failedMessages; - @Nullable - private final Exception[] messageExceptions; + private final Exception @Nullable [] messageExceptions; /** @@ -124,8 +124,7 @@ public final Exception[] getMessageExceptions() { @Override - @Nullable - public String getMessage() { + public @Nullable String getMessage() { if (ObjectUtils.isEmpty(this.messageExceptions)) { return super.getMessage(); } diff --git a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java index d86db63adb0a..997b20356d61 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/SimpleMailMessage.java @@ -19,7 +19,8 @@ import java.io.Serializable; import java.util.Date; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -44,29 +45,21 @@ @SuppressWarnings("serial") public class SimpleMailMessage implements MailMessage, Serializable { - @Nullable - private String from; + private @Nullable String from; - @Nullable - private String replyTo; + private @Nullable String replyTo; - @Nullable - private String[] to; + private String @Nullable [] to; - @Nullable - private String[] cc; + private String @Nullable [] cc; - @Nullable - private String[] bcc; + private String @Nullable [] bcc; - @Nullable - private Date sentDate; + private @Nullable Date sentDate; - @Nullable - private String subject; + private @Nullable String subject; - @Nullable - private String text; + private @Nullable String text; /** @@ -97,8 +90,7 @@ public void setFrom(@Nullable String from) { this.from = from; } - @Nullable - public String getFrom() { + public @Nullable String getFrom() { return this.from; } @@ -107,8 +99,7 @@ public void setReplyTo(@Nullable String replyTo) { this.replyTo = replyTo; } - @Nullable - public String getReplyTo() { + public @Nullable String getReplyTo() { return this.replyTo; } @@ -122,8 +113,7 @@ public void setTo(String... to) { this.to = to; } - @Nullable - public String[] getTo() { + public String @Nullable [] getTo() { return this.to; } @@ -133,12 +123,11 @@ public void setCc(@Nullable String cc) { } @Override - public void setCc(@Nullable String... cc) { + public void setCc(String @Nullable ... cc) { this.cc = cc; } - @Nullable - public String[] getCc() { + public String @Nullable [] getCc() { return this.cc; } @@ -148,12 +137,11 @@ public void setBcc(@Nullable String bcc) { } @Override - public void setBcc(@Nullable String... bcc) { + public void setBcc(String @Nullable ... bcc) { this.bcc = bcc; } - @Nullable - public String[] getBcc() { + public String @Nullable [] getBcc() { return this.bcc; } @@ -162,8 +150,7 @@ public void setSentDate(@Nullable Date sentDate) { this.sentDate = sentDate; } - @Nullable - public Date getSentDate() { + public @Nullable Date getSentDate() { return this.sentDate; } @@ -172,8 +159,7 @@ public void setSubject(@Nullable String subject) { this.subject = subject; } - @Nullable - public String getSubject() { + public @Nullable String getSubject() { return this.subject; } @@ -182,8 +168,7 @@ public void setText(@Nullable String text) { this.text = text; } - @Nullable - public String getText() { + public @Nullable String getText() { return this.text; } @@ -255,8 +240,7 @@ public String toString() { } - @Nullable - private static String[] copyOrNull(@Nullable String[] state) { + private static String @Nullable [] copyOrNull(String @Nullable [] state) { if (state == null) { return null; } diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java index 7fe0a9aca896..97771d852355 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/ConfigurableMimeFileTypeMap.java @@ -22,11 +22,11 @@ import jakarta.activation.FileTypeMap; import jakarta.activation.MimetypesFileTypeMap; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Spring-configurable {@code FileTypeMap} implementation that will read @@ -70,15 +70,13 @@ public class ConfigurableMimeFileTypeMap extends FileTypeMap implements Initiali /** * Used to configure additional mappings. */ - @Nullable - private String[] mappings; + private String @Nullable [] mappings; /** * The delegate FileTypeMap, compiled from the mappings in the mapping file * and the entries in the {@code mappings} property. */ - @Nullable - private FileTypeMap fileTypeMap; + private @Nullable FileTypeMap fileTypeMap; /** @@ -143,7 +141,7 @@ protected final FileTypeMap getFileTypeMap() { * @see jakarta.activation.MimetypesFileTypeMap#MimetypesFileTypeMap(java.io.InputStream) * @see jakarta.activation.MimetypesFileTypeMap#addMimeTypes(String) */ - protected FileTypeMap createFileTypeMap(@Nullable Resource mappingLocation, @Nullable String[] mappings) throws IOException { + protected FileTypeMap createFileTypeMap(@Nullable Resource mappingLocation, String @Nullable [] mappings) throws IOException { MimetypesFileTypeMap fileTypeMap = null; if (mappingLocation != null) { try (InputStream is = mappingLocation.getInputStream()) { diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java index 165009856082..a21665ec3d55 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailMimeTypesRuntimeHints.java @@ -16,9 +16,10 @@ package org.springframework.mail.javamail; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} implementation that makes sure mime types diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java index 333da35987bb..e89cd78e0670 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/JavaMailSenderImpl.java @@ -32,8 +32,8 @@ import jakarta.mail.Session; import jakarta.mail.Transport; import jakarta.mail.internet.MimeMessage; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mail.MailAuthenticationException; import org.springframework.mail.MailException; import org.springframework.mail.MailParseException; @@ -80,28 +80,21 @@ public class JavaMailSenderImpl implements JavaMailSender { private Properties javaMailProperties = new Properties(); - @Nullable - private Session session; + private @Nullable Session session; - @Nullable - private String protocol; + private @Nullable String protocol; - @Nullable - private String host; + private @Nullable String host; private int port = DEFAULT_PORT; - @Nullable - private String username; + private @Nullable String username; - @Nullable - private String password; + private @Nullable String password; - @Nullable - private String defaultEncoding; + private @Nullable String defaultEncoding; - @Nullable - private FileTypeMap defaultFileTypeMap; + private @Nullable FileTypeMap defaultFileTypeMap; /** @@ -174,8 +167,7 @@ public void setProtocol(@Nullable String protocol) { /** * Return the mail protocol. */ - @Nullable - public String getProtocol() { + public @Nullable String getProtocol() { return this.protocol; } @@ -190,8 +182,7 @@ public void setHost(@Nullable String host) { /** * Return the mail server host. */ - @Nullable - public String getHost() { + public @Nullable String getHost() { return this.host; } @@ -229,8 +220,7 @@ public void setUsername(@Nullable String username) { /** * Return the username for the account at the mail host. */ - @Nullable - public String getUsername() { + public @Nullable String getUsername() { return this.username; } @@ -252,8 +242,7 @@ public void setPassword(@Nullable String password) { /** * Return the password for the account at the mail host. */ - @Nullable - public String getPassword() { + public @Nullable String getPassword() { return this.password; } @@ -270,8 +259,7 @@ public void setDefaultEncoding(@Nullable String defaultEncoding) { * Return the default encoding for {@link MimeMessage MimeMessages}, * or {@code null} if none. */ - @Nullable - public String getDefaultEncoding() { + public @Nullable String getDefaultEncoding() { return this.defaultEncoding; } @@ -296,8 +284,7 @@ public void setDefaultFileTypeMap(@Nullable FileTypeMap defaultFileTypeMap) { * Return the default Java Activation {@link FileTypeMap} for * {@link MimeMessage MimeMessages}, or {@code null} if none. */ - @Nullable - public FileTypeMap getDefaultFileTypeMap() { + public @Nullable FileTypeMap getDefaultFileTypeMap() { return this.defaultFileTypeMap; } @@ -377,7 +364,7 @@ public void testConnection() throws MessagingException { * @throws org.springframework.mail.MailSendException * in case of failure when sending a message */ - protected void doSend(MimeMessage[] mimeMessages, @Nullable Object[] originalMessages) throws MailException { + protected void doSend(MimeMessage[] mimeMessages, Object @Nullable [] originalMessages) throws MailException { Map failedMessages = new LinkedHashMap<>(); Transport transport = null; diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java index 302a401f7310..eea388cc1fb7 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/MimeMessageHelper.java @@ -38,10 +38,10 @@ import jakarta.mail.internet.MimeMultipart; import jakarta.mail.internet.MimePart; import jakarta.mail.internet.MimeUtility; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.InputStreamSource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeTypeUtils; @@ -165,14 +165,11 @@ public class MimeMessageHelper { private final MimeMessage mimeMessage; - @Nullable - private MimeMultipart rootMimeMultipart; + private @Nullable MimeMultipart rootMimeMultipart; - @Nullable - private MimeMultipart mimeMultipart; + private @Nullable MimeMultipart mimeMultipart; - @Nullable - private final String encoding; + private final @Nullable String encoding; private FileTypeMap fileTypeMap; @@ -426,8 +423,7 @@ public final MimeMultipart getMimeMultipart() throws IllegalStateException { * @return the default encoding associated with the MimeMessage, * or {@code null} if none found */ - @Nullable - protected String getDefaultEncoding(MimeMessage mimeMessage) { + protected @Nullable String getDefaultEncoding(MimeMessage mimeMessage) { if (mimeMessage instanceof SmartMimeMessage smartMimeMessage) { return smartMimeMessage.getDefaultEncoding(); } @@ -437,8 +433,7 @@ protected String getDefaultEncoding(MimeMessage mimeMessage) { /** * Return the specific character encoding used for this message, if any. */ - @Nullable - public String getEncoding() { + public @Nullable String getEncoding() { return this.encoding; } diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java index e41d4a226558..09625057a8a4 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/SmartMimeMessage.java @@ -19,8 +19,7 @@ import jakarta.activation.FileTypeMap; import jakarta.mail.Session; import jakarta.mail.internet.MimeMessage; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Special subclass of the standard JavaMail {@link MimeMessage}, carrying a @@ -39,11 +38,9 @@ */ class SmartMimeMessage extends MimeMessage { - @Nullable - private final String defaultEncoding; + private final @Nullable String defaultEncoding; - @Nullable - private final FileTypeMap defaultFileTypeMap; + private final @Nullable FileTypeMap defaultFileTypeMap; /** @@ -64,16 +61,14 @@ public SmartMimeMessage( /** * Return the default encoding of this message, or {@code null} if none. */ - @Nullable - public final String getDefaultEncoding() { + public final @Nullable String getDefaultEncoding() { return this.defaultEncoding; } /** * Return the default FileTypeMap of this message, or {@code null} if none. */ - @Nullable - public final FileTypeMap getDefaultFileTypeMap() { + public final @Nullable FileTypeMap getDefaultFileTypeMap() { return this.defaultFileTypeMap; } diff --git a/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java b/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java index e5114480e030..280fd4f1d4db 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/mail/javamail/package-info.java @@ -3,9 +3,7 @@ * Provides an extended JavaMailSender interface and a MimeMessageHelper * class for convenient population of a JavaMail MimeMessage. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mail.javamail; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/mail/package-info.java b/spring-context-support/src/main/java/org/springframework/mail/package-info.java index a5d452deb96e..fce30404d900 100644 --- a/spring-context-support/src/main/java/org/springframework/mail/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/mail/package-info.java @@ -2,9 +2,7 @@ * Spring's generic mail infrastructure. * Concrete implementations are provided in the subpackages. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mail; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java index 73d12fab6927..fe86676a7ee6 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/CronTriggerFactoryBean.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.TimeZone; +import org.jspecify.annotations.Nullable; import org.quartz.CronTrigger; import org.quartz.JobDataMap; import org.quartz.JobDetail; @@ -30,7 +31,6 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -70,43 +70,33 @@ public class CronTriggerFactoryBean implements FactoryBean, BeanNam ); - @Nullable - private String name; + private @Nullable String name; - @Nullable - private String group; + private @Nullable String group; - @Nullable - private JobDetail jobDetail; + private @Nullable JobDetail jobDetail; private JobDataMap jobDataMap = new JobDataMap(); - @Nullable - private Date startTime; + private @Nullable Date startTime; private long startDelay = 0; - @Nullable - private String cronExpression; + private @Nullable String cronExpression; - @Nullable - private TimeZone timeZone; + private @Nullable TimeZone timeZone; - @Nullable - private String calendarName; + private @Nullable String calendarName; private int priority; private int misfireInstruction = CronTrigger.MISFIRE_INSTRUCTION_SMART_POLICY; - @Nullable - private String description; + private @Nullable String description; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private CronTrigger cronTrigger; + private @Nullable CronTrigger cronTrigger; /** @@ -281,8 +271,7 @@ public void afterPropertiesSet() throws ParseException { @Override - @Nullable - public CronTrigger getObject() { + public @Nullable CronTrigger getObject() { return this.cronTrigger; } diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java index 32ee0b90de7b..dc15170e8627 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/JobDetailFactoryBean.java @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.quartz.Job; import org.quartz.JobDataMap; import org.quartz.JobDetail; @@ -29,7 +30,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -50,14 +50,11 @@ public class JobDetailFactoryBean implements FactoryBean, BeanNameAware, ApplicationContextAware, InitializingBean { - @Nullable - private String name; + private @Nullable String name; - @Nullable - private String group; + private @Nullable String group; - @Nullable - private Class jobClass; + private @Nullable Class jobClass; private JobDataMap jobDataMap = new JobDataMap(); @@ -65,20 +62,15 @@ public class JobDetailFactoryBean private boolean requestsRecovery = false; - @Nullable - private String description; + private @Nullable String description; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private String applicationContextJobDataKey; + private @Nullable String applicationContextJobDataKey; - @Nullable - private JobDetail jobDetail; + private @Nullable JobDetail jobDetail; /** @@ -218,8 +210,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public JobDetail getObject() { + public @Nullable JobDetail getObject() { return this.jobDetail; } diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java index fc8875b58111..2a342916edf7 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalDataSourceJobStore.java @@ -23,6 +23,7 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.quartz.SchedulerConfigException; import org.quartz.impl.jdbcjobstore.JobStoreCMT; import org.quartz.impl.jdbcjobstore.SimpleSemaphore; @@ -34,7 +35,6 @@ import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.MetaDataAccessException; -import org.springframework.lang.Nullable; /** * Subclass of Quartz's {@link JobStoreCMT} class that delegates to a Spring-managed @@ -86,12 +86,11 @@ public class LocalDataSourceJobStore extends JobStoreCMT { public static final String NON_TX_DATA_SOURCE_PREFIX = "springNonTxDataSource."; - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void initialize(ClassLoadHelper loadHelper, SchedulerSignaler signaler) throws SchedulerConfigException { // Absolutely needs thread-bound DataSource to initialize. this.dataSource = SchedulerFactoryBean.getConfigTimeDataSource(); diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java index 6034e47822bf..23476846ae12 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/LocalTaskExecutorThreadPool.java @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.quartz.SchedulerConfigException; import org.quartz.spi.ThreadPool; import org.springframework.aot.hint.annotation.Reflective; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -41,8 +41,7 @@ public class LocalTaskExecutorThreadPool implements ThreadPool { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Executor taskExecutor; + private @Nullable Executor taskExecutor; @Override diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java index d692c4bf74f8..1185b80508d2 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/MethodInvokingJobDetailFactoryBean.java @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.quartz.DisallowConcurrentExecution; import org.quartz.Job; import org.quartz.JobDetail; @@ -36,7 +37,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.support.ArgumentConvertingMethodInvoker; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MethodInvoker; @@ -78,27 +78,21 @@ public class MethodInvokingJobDetailFactoryBean extends ArgumentConvertingMethodInvoker implements FactoryBean, BeanNameAware, BeanClassLoaderAware, BeanFactoryAware, InitializingBean { - @Nullable - private String name; + private @Nullable String name; private String group = Scheduler.DEFAULT_GROUP; private boolean concurrent = true; - @Nullable - private String targetBeanName; + private @Nullable String targetBeanName; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private JobDetail jobDetail; + private @Nullable JobDetail jobDetail; /** @@ -199,8 +193,7 @@ protected void postProcessJobDetail(JobDetail jobDetail) { * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. */ @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { Class targetClass = super.getTargetClass(); if (targetClass == null && this.targetBeanName != null) { Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'"); @@ -213,8 +206,7 @@ public Class getTargetClass() { * Overridden to support the {@link #setTargetBeanName "targetBeanName"} feature. */ @Override - @Nullable - public Object getTargetObject() { + public @Nullable Object getTargetObject() { Object targetObject = super.getTargetObject(); if (targetObject == null && this.targetBeanName != null) { Assert.state(this.beanFactory != null, "BeanFactory must be set when using 'targetBeanName'"); @@ -225,8 +217,7 @@ public Object getTargetObject() { @Override - @Nullable - public JobDetail getObject() { + public @Nullable JobDetail getObject() { return this.jobDetail; } @@ -249,8 +240,7 @@ public static class MethodInvokingJob extends QuartzJobBean { protected static final Log logger = LogFactory.getLog(MethodInvokingJob.class); - @Nullable - private MethodInvoker methodInvoker; + private @Nullable MethodInvoker methodInvoker; /** * Set the MethodInvoker to use. diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java index 996a598460d2..8be7e10533fe 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/ResourceLoaderClassLoadHelper.java @@ -22,12 +22,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.quartz.spi.ClassLoadHelper; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -44,8 +44,7 @@ public class ResourceLoaderClassLoadHelper implements ClassLoadHelper { protected static final Log logger = LogFactory.getLog(ResourceLoaderClassLoadHelper.class); - @Nullable - private ResourceLoader resourceLoader; + private @Nullable ResourceLoader resourceLoader; /** @@ -88,8 +87,7 @@ public Class loadClass(String name, Class clazz) throws Clas } @Override - @Nullable - public URL getResource(String name) { + public @Nullable URL getResource(String name) { Assert.state(this.resourceLoader != null, "ResourceLoaderClassLoadHelper not initialized"); Resource resource = this.resourceLoader.getResource(name); if (resource.exists()) { @@ -109,8 +107,7 @@ public URL getResource(String name) { } @Override - @Nullable - public InputStream getResourceAsStream(String name) { + public @Nullable InputStream getResourceAsStream(String name) { Assert.state(this.resourceLoader != null, "ResourceLoaderClassLoadHelper not initialized"); Resource resource = this.resourceLoader.getResource(name); if (resource.exists()) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java index e6b1c7aa5437..33dd9281209a 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.quartz.Calendar; import org.quartz.JobDetail; import org.quartz.JobListener; @@ -38,7 +39,6 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -63,32 +63,23 @@ public abstract class SchedulerAccessor implements ResourceLoaderAware { private boolean overwriteExistingJobs = false; - @Nullable - private String[] jobSchedulingDataLocations; + private String @Nullable [] jobSchedulingDataLocations; - @Nullable - private List jobDetails; + private @Nullable List jobDetails; - @Nullable - private Map calendars; + private @Nullable Map calendars; - @Nullable - private List triggers; + private @Nullable List triggers; - @Nullable - private SchedulerListener[] schedulerListeners; + private SchedulerListener @Nullable [] schedulerListeners; - @Nullable - private JobListener[] globalJobListeners; + private JobListener @Nullable [] globalJobListeners; - @Nullable - private TriggerListener[] globalTriggerListeners; + private TriggerListener @Nullable [] globalTriggerListeners; - @Nullable - private PlatformTransactionManager transactionManager; + private @Nullable PlatformTransactionManager transactionManager; - @Nullable - protected ResourceLoader resourceLoader; + protected @Nullable ResourceLoader resourceLoader; /** @@ -203,7 +194,7 @@ public void setResourceLoader(ResourceLoader resourceLoader) { /** * Register jobs and triggers (within a transaction, if possible). */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void registerJobsAndTriggers() throws SchedulerException { TransactionStatus transactionStatus = null; if (this.transactionManager != null) { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java index 83a8b47112d0..2893a8635d88 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerAccessorBean.java @@ -16,6 +16,7 @@ package org.springframework.scheduling.quartz; +import org.jspecify.annotations.Nullable; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.impl.SchedulerRepository; @@ -24,7 +25,6 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -40,14 +40,11 @@ */ public class SchedulerAccessorBean extends SchedulerAccessor implements BeanFactoryAware, InitializingBean { - @Nullable - private String schedulerName; + private @Nullable String schedulerName; - @Nullable - private Scheduler scheduler; + private @Nullable Scheduler scheduler; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java index ddc740a25b28..44e2e4ac98c7 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBean.java @@ -24,6 +24,7 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.quartz.Scheduler; import org.quartz.SchedulerException; import org.quartz.SchedulerFactory; @@ -44,7 +45,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingException; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -119,8 +119,7 @@ public class SchedulerFactoryBean extends SchedulerAccessor implements FactoryBe * @see #setApplicationContext * @see ResourceLoaderClassLoadHelper */ - @Nullable - public static ResourceLoader getConfigTimeResourceLoader() { + public static @Nullable ResourceLoader getConfigTimeResourceLoader() { return configTimeResourceLoaderHolder.get(); } @@ -133,8 +132,7 @@ public static ResourceLoader getConfigTimeResourceLoader() { * @see #setTaskExecutor * @see LocalTaskExecutorThreadPool */ - @Nullable - public static Executor getConfigTimeTaskExecutor() { + public static @Nullable Executor getConfigTimeTaskExecutor() { return configTimeTaskExecutorHolder.get(); } @@ -147,8 +145,7 @@ public static Executor getConfigTimeTaskExecutor() { * @see #setDataSource * @see LocalDataSourceJobStore */ - @Nullable - public static DataSource getConfigTimeDataSource() { + public static @Nullable DataSource getConfigTimeDataSource() { return configTimeDataSourceHolder.get(); } @@ -161,43 +158,32 @@ public static DataSource getConfigTimeDataSource() { * @see #setNonTransactionalDataSource * @see LocalDataSourceJobStore */ - @Nullable - public static DataSource getConfigTimeNonTransactionalDataSource() { + public static @Nullable DataSource getConfigTimeNonTransactionalDataSource() { return configTimeNonTransactionalDataSourceHolder.get(); } - @Nullable - private SchedulerFactory schedulerFactory; + private @Nullable SchedulerFactory schedulerFactory; private Class schedulerFactoryClass = StdSchedulerFactory.class; - @Nullable - private String schedulerName; + private @Nullable String schedulerName; - @Nullable - private Resource configLocation; + private @Nullable Resource configLocation; - @Nullable - private Properties quartzProperties; + private @Nullable Properties quartzProperties; - @Nullable - private Executor taskExecutor; + private @Nullable Executor taskExecutor; - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; - @Nullable - private DataSource nonTransactionalDataSource; + private @Nullable DataSource nonTransactionalDataSource; - @Nullable - private Map schedulerContextMap; + private @Nullable Map schedulerContextMap; - @Nullable - private String applicationContextSchedulerContextKey; + private @Nullable String applicationContextSchedulerContextKey; - @Nullable - private JobFactory jobFactory; + private @Nullable JobFactory jobFactory; private boolean jobFactorySet = false; @@ -211,14 +197,11 @@ public static DataSource getConfigTimeNonTransactionalDataSource() { private boolean waitForJobsToCompleteOnShutdown = false; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private Scheduler scheduler; + private @Nullable Scheduler scheduler; /** @@ -661,7 +644,7 @@ private Scheduler prepareScheduler(SchedulerFactory schedulerFactory) throws Sch * @see #afterPropertiesSet * @see org.quartz.SchedulerFactory#getScheduler */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected Scheduler createScheduler(SchedulerFactory schedulerFactory, @Nullable String schedulerName) throws SchedulerException { @@ -773,8 +756,7 @@ public Scheduler getScheduler() { } @Override - @Nullable - public Scheduler getObject() { + public @Nullable Scheduler getObject() { return this.scheduler; } diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java index ee5e43c05c39..9030a61b0420 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SchedulerFactoryBeanRuntimeHints.java @@ -16,13 +16,14 @@ package org.springframework.scheduling.quartz; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeHint.Builder; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.annotation.ReflectiveRuntimeHintsRegistrar; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java index 9ab8c18cfa34..b728c7e72096 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleThreadPoolTaskExecutor.java @@ -25,12 +25,10 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.SchedulingException; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; /** * Subclass of Quartz's SimpleThreadPool that implements Spring's @@ -47,9 +45,9 @@ * @see org.springframework.core.task.TaskExecutor * @see SchedulerFactoryBean#setTaskExecutor */ -@SuppressWarnings({"deprecation", "removal"}) +@SuppressWarnings("deprecation") public class SimpleThreadPoolTaskExecutor extends SimpleThreadPool - implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, InitializingBean, DisposableBean { + implements AsyncTaskExecutor, SchedulingTaskExecutor, InitializingBean, DisposableBean { private boolean waitForJobsToCompleteOnShutdown = false; @@ -91,20 +89,6 @@ public Future submit(Callable task) { return future; } - @Override - public ListenableFuture submitListenable(Runnable task) { - ListenableFutureTask future = new ListenableFutureTask<>(task, null); - execute(future); - return future; - } - - @Override - public ListenableFuture submitListenable(Callable task) { - ListenableFutureTask future = new ListenableFutureTask<>(task); - execute(future); - return future; - } - @Override public void destroy() { diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java index e16a32f59677..fe230490c461 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SimpleTriggerFactoryBean.java @@ -19,6 +19,7 @@ import java.util.Date; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.quartz.JobDataMap; import org.quartz.JobDetail; import org.quartz.Scheduler; @@ -28,7 +29,6 @@ import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -77,19 +77,15 @@ public class SimpleTriggerFactoryBean implements FactoryBean, Bea SimpleTrigger.MISFIRE_INSTRUCTION_RESCHEDULE_NOW_WITH_REMAINING_REPEAT_COUNT ); - @Nullable - private String name; + private @Nullable String name; - @Nullable - private String group; + private @Nullable String group; - @Nullable - private JobDetail jobDetail; + private @Nullable JobDetail jobDetail; private JobDataMap jobDataMap = new JobDataMap(); - @Nullable - private Date startTime; + private @Nullable Date startTime; private long startDelay; @@ -101,14 +97,11 @@ public class SimpleTriggerFactoryBean implements FactoryBean, Bea private int misfireInstruction = SimpleTrigger.MISFIRE_INSTRUCTION_SMART_POLICY; - @Nullable - private String description; + private @Nullable String description; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private SimpleTrigger simpleTrigger; + private @Nullable SimpleTrigger simpleTrigger; /** @@ -275,8 +268,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public SimpleTrigger getObject() { + public @Nullable SimpleTrigger getObject() { return this.simpleTrigger; } diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java index 2d48a2258ae0..cc20770935a2 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/SpringBeanJobFactory.java @@ -16,6 +16,7 @@ package org.springframework.scheduling.quartz; +import org.jspecify.annotations.Nullable; import org.quartz.SchedulerContext; import org.quartz.spi.TriggerFiredBundle; @@ -24,7 +25,6 @@ import org.springframework.beans.PropertyAccessorFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.Nullable; /** * Subclass of {@link AdaptableJobFactory} that also supports Spring-style @@ -46,14 +46,11 @@ public class SpringBeanJobFactory extends AdaptableJobFactory implements ApplicationContextAware, SchedulerContextAware { - @Nullable - private String[] ignoredUnknownProperties; + private String @Nullable [] ignoredUnknownProperties; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private SchedulerContext schedulerContext; + private @Nullable SchedulerContext schedulerContext; /** diff --git a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java index 6ca38a31963d..a7bbe32ec8c5 100644 --- a/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/scheduling/quartz/package-info.java @@ -5,9 +5,7 @@ * Triggers as beans in a Spring context. Also provides * convenience classes for implementing Quartz Jobs. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling.quartz; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java index 5b0405c5f401..be9a8f1db70e 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactory.java @@ -33,12 +33,12 @@ import freemarker.template.TemplateException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -81,28 +81,21 @@ public class FreeMarkerConfigurationFactory { protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Resource configLocation; + private @Nullable Resource configLocation; - @Nullable - private Properties freemarkerSettings; + private @Nullable Properties freemarkerSettings; - @Nullable - private Map freemarkerVariables; + private @Nullable Map freemarkerVariables; - @Nullable - private String defaultEncoding; + private @Nullable String defaultEncoding; private final List templateLoaders = new ArrayList<>(); - @Nullable - private List preTemplateLoaders; + private @Nullable List preTemplateLoaders; - @Nullable - private List postTemplateLoaders; + private @Nullable List postTemplateLoaders; - @Nullable - private String[] templateLoaderPaths; + private String @Nullable [] templateLoaderPaths; private ResourceLoader resourceLoader = new DefaultResourceLoader(); @@ -418,8 +411,7 @@ protected void postProcessTemplateLoaders(List templateLoaders) * @param templateLoaders the final List of {@code TemplateLoader} instances * @return the aggregate TemplateLoader */ - @Nullable - protected TemplateLoader getAggregateTemplateLoader(List templateLoaders) { + protected @Nullable TemplateLoader getAggregateTemplateLoader(List templateLoaders) { return switch (templateLoaders.size()) { case 0 -> { logger.debug("No FreeMarker TemplateLoaders specified"); diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java index 6a35e066ec4a..796dc4bc6a4c 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/FreeMarkerConfigurationFactoryBean.java @@ -20,11 +20,11 @@ import freemarker.template.Configuration; import freemarker.template.TemplateException; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; -import org.springframework.lang.Nullable; /** * Factory bean that creates a FreeMarker {@link Configuration} and provides it @@ -57,8 +57,7 @@ public class FreeMarkerConfigurationFactoryBean extends FreeMarkerConfigurationFactory implements FactoryBean, InitializingBean, ResourceLoaderAware { - @Nullable - private Configuration configuration; + private @Nullable Configuration configuration; @Override @@ -68,8 +67,7 @@ public void afterPropertiesSet() throws IOException, TemplateException { @Override - @Nullable - public Configuration getObject() { + public @Nullable Configuration getObject() { return this.configuration; } diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java index 1749b0a37d94..f58470949ac8 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/SpringTemplateLoader.java @@ -23,10 +23,10 @@ import freemarker.cache.TemplateLoader; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; /** * FreeMarker {@link TemplateLoader} adapter that loads template files via a @@ -68,8 +68,7 @@ public SpringTemplateLoader(ResourceLoader resourceLoader, String templateLoader @Override - @Nullable - public Object findTemplateSource(String name) throws IOException { + public @Nullable Object findTemplateSource(String name) throws IOException { if (logger.isDebugEnabled()) { logger.debug("Looking for FreeMarker template with name [" + name + "]"); } diff --git a/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java b/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java index 492946e8998f..20352c9f84f0 100644 --- a/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java +++ b/spring-context-support/src/main/java/org/springframework/ui/freemarker/package-info.java @@ -3,9 +3,7 @@ * FreeMarker * within a Spring application context. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.ui.freemarker; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java index 232a6b007624..39983d680a2f 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheKeyGeneratorTests.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,7 +17,6 @@ package org.springframework.cache.jcache.interceptor; import java.lang.reflect.Method; -import java.util.Arrays; import java.util.concurrent.atomic.AtomicLong; import javax.cache.annotation.CacheDefaults; @@ -150,9 +149,9 @@ private void expect(Object... params) { @Override public Object generate(Object target, Method method, Object... params) { - assertThat(Arrays.equals(expectedParams, params)).as("Unexpected parameters: expected: " - + Arrays.toString(this.expectedParams) + " but got: " + Arrays.toString(params)).isTrue(); + assertThat(params).as("Unexpected parameters").isEqualTo(expectedParams); return new SimpleKey(params); } } + } diff --git a/spring-context/spring-context.gradle b/spring-context/spring-context.gradle index af48a0fa2070..5c50407bf9c1 100644 --- a/spring-context/spring-context.gradle +++ b/spring-context/spring-context.gradle @@ -12,6 +12,7 @@ dependencies { api(project(":spring-core")) api(project(":spring-expression")) api("io.micrometer:micrometer-observation") + compileOnly("com.google.code.findbugs:jsr305") // for Micrometer context-propagation optional(project(":spring-instrument")) optional("io.micrometer:context-propagation") optional("io.projectreactor:reactor-core") @@ -21,21 +22,18 @@ dependencies { optional("jakarta.inject:jakarta.inject-api") optional("jakarta.interceptor:jakarta.interceptor-api") optional("jakarta.validation:jakarta.validation-api") - optional("javax.annotation:javax.annotation-api") - optional("javax.inject:javax.inject") optional("javax.money:money-api") optional("org.apache.groovy:groovy") optional("org.apache-extras.beanshell:bsh") optional("org.aspectj:aspectjweaver") optional("org.crac:crac") - optional("org.hibernate:hibernate-validator") + optional("org.hibernate.validator:hibernate-validator") optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") optional("org.reactivestreams:reactive-streams") testFixturesApi("org.junit.jupiter:junit-jupiter-api") testFixturesImplementation(testFixtures(project(":spring-beans"))) - testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("io.projectreactor:reactor-test") testFixturesImplementation("org.assertj:assertj-core") testImplementation(project(":spring-core-test")) @@ -49,6 +47,7 @@ dependencies { testImplementation("org.awaitility:awaitility") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") + testImplementation("io.projectreactor:reactor-test") testImplementation("io.reactivex.rxjava3:rxjava") testImplementation('io.micrometer:context-propagation') testImplementation("io.micrometer:micrometer-observation-test") @@ -59,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/Cache.java b/spring-context/src/main/java/org/springframework/cache/Cache.java index 171616604d39..03ae0ead6f7f 100644 --- a/spring-context/src/main/java/org/springframework/cache/Cache.java +++ b/spring-context/src/main/java/org/springframework/cache/Cache.java @@ -20,7 +20,7 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that defines common cache operations. @@ -65,8 +65,7 @@ public interface Cache { * @see #get(Object, Class) * @see #get(Object, Callable) */ - @Nullable - ValueWrapper get(Object key); + @Nullable ValueWrapper get(Object key); /** * Return the value to which this cache maps the specified key, @@ -86,8 +85,7 @@ public interface Cache { * @since 4.0 * @see #get(Object) */ - @Nullable - T get(Object key, @Nullable Class type); + @Nullable T get(Object key, @Nullable Class type); /** * Return the value to which this cache maps the specified key, obtaining @@ -105,8 +103,7 @@ public interface Cache { * @since 4.3 * @see #get(Object) */ - @Nullable - T get(Object key, Callable valueLoader); + @Nullable T get(Object key, Callable valueLoader); /** * Return the value to which this cache maps the specified key, @@ -136,8 +133,7 @@ public interface Cache { * @since 6.1 * @see #retrieve(Object, Supplier) */ - @Nullable - default CompletableFuture retrieve(Object key) { + default @Nullable CompletableFuture retrieve(Object key) { throw new UnsupportedOperationException( getClass().getName() + " does not support CompletableFuture-based retrieval"); } @@ -214,8 +210,7 @@ default CompletableFuture retrieve(Object key, Supplier loader, @Nullable Throwable ex) { super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); this.key = key; } - @Nullable - public Object getKey() { + public @Nullable Object getKey() { return this.key; } } diff --git a/spring-context/src/main/java/org/springframework/cache/CacheManager.java b/spring-context/src/main/java/org/springframework/cache/CacheManager.java index 833715c63cb6..ca7af761d145 100644 --- a/spring-context/src/main/java/org/springframework/cache/CacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/CacheManager.java @@ -18,7 +18,7 @@ import java.util.Collection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Spring's central cache manager SPI. @@ -39,8 +39,7 @@ public interface CacheManager { * @return the associated cache, or {@code null} if such a cache * does not exist or could be not created */ - @Nullable - Cache getCache(String name); + @Nullable Cache getCache(String name); /** * Get a collection of the cache names known by this manager. diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java index 37eede6f2a5b..824773f8928b 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.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. @@ -20,6 +20,8 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; @@ -30,7 +32,6 @@ import org.springframework.context.annotation.ImportAware; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.function.SingletonSupplier; @@ -47,20 +48,15 @@ @Configuration(proxyBeanMethods = false) public abstract class AbstractCachingConfiguration implements ImportAware { - @Nullable - protected AnnotationAttributes enableCaching; + protected @Nullable AnnotationAttributes enableCaching; - @Nullable - protected Supplier cacheManager; + protected @Nullable Supplier<@Nullable CacheManager> cacheManager; - @Nullable - protected Supplier cacheResolver; + protected @Nullable Supplier<@Nullable CacheResolver> cacheResolver; - @Nullable - protected Supplier keyGenerator; + protected @Nullable Supplier<@Nullable KeyGenerator> keyGenerator; - @Nullable - protected Supplier errorHandler; + protected @Nullable Supplier<@Nullable CacheErrorHandler> errorHandler; @Override @@ -75,7 +71,7 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { @Autowired void setConfigurers(ObjectProvider configurers) { - Supplier configurer = () -> { + Supplier<@Nullable CachingConfigurer> configurer = () -> { List candidates = configurers.stream().toList(); if (CollectionUtils.isEmpty(candidates)) { return null; @@ -94,6 +90,7 @@ void setConfigurers(ObjectProvider configurers) { /** * Extract the configuration from the nominated {@link CachingConfigurer}. */ + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerSupplier) { this.cacheManager = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheManager); this.cacheResolver = cachingConfigurerSupplier.adapt(CachingConfigurer::cacheResolver); @@ -104,10 +101,10 @@ protected void useCachingConfigurer(CachingConfigurerSupplier cachingConfigurerS protected static class CachingConfigurerSupplier { - private final Supplier supplier; + private final SingletonSupplier supplier; - public CachingConfigurerSupplier(Supplier supplier) { - this.supplier = SingletonSupplier.of(supplier); + public CachingConfigurerSupplier(Supplier<@Nullable CachingConfigurer> supplier) { + this.supplier = SingletonSupplier.ofNullable(supplier); } /** @@ -119,8 +116,7 @@ public CachingConfigurerSupplier(Supplier supplier) { * @param the type of the supplier * @return another supplier mapped by the specified function */ - @Nullable - public Supplier adapt(Function provider) { + public Supplier<@Nullable T> adapt(Function provider) { return () -> { CachingConfigurer cachingConfigurer = this.supplier.get(); return (cachingConfigurer != null ? provider.apply(cachingConfigurer) : null); diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java index 8f26e688a673..439721958fb7 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AnnotationCacheOperationSource.java @@ -23,9 +23,10 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource; import org.springframework.cache.interceptor.CacheOperation; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -120,14 +121,12 @@ public boolean isCandidateClass(Class targetClass) { } @Override - @Nullable - protected Collection findCacheOperations(Class clazz) { + protected @Nullable Collection findCacheOperations(Class clazz) { return determineCacheOperations(parser -> parser.parseCacheAnnotations(clazz)); } @Override - @Nullable - protected Collection findCacheOperations(Method method) { + protected @Nullable Collection findCacheOperations(Method method) { return determineCacheOperations(parser -> parser.parseCacheAnnotations(method)); } @@ -140,8 +139,7 @@ protected Collection findCacheOperations(Method method) { * @param provider the cache operation provider to use * @return the configured caching operations, or {@code null} if none found */ - @Nullable - protected Collection determineCacheOperations(CacheOperationProvider provider) { + protected @Nullable Collection determineCacheOperations(CacheOperationProvider provider) { Collection ops = null; for (CacheAnnotationParser parser : this.annotationParsers) { Collection annOps = provider.getCacheOperations(parser); @@ -195,8 +193,7 @@ protected interface CacheOperationProvider { * @param parser the parser to use * @return the cache operations, or {@code null} if none found */ - @Nullable - Collection getCacheOperations(CacheAnnotationParser parser); + @Nullable Collection getCacheOperations(CacheAnnotationParser parser); } } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java index 10231a157131..158da40e6d9f 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CacheAnnotationParser.java @@ -19,8 +19,9 @@ import java.lang.reflect.Method; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheOperation; -import org.springframework.lang.Nullable; /** * Strategy interface for parsing known caching annotation types. @@ -64,8 +65,7 @@ default boolean isCandidateClass(Class targetClass) { * @return the configured caching operation, or {@code null} if none found * @see AnnotationCacheOperationSource#findCacheOperations(Class) */ - @Nullable - Collection parseCacheAnnotations(Class type); + @Nullable Collection parseCacheAnnotations(Class type); /** * Parse the cache definition for the given method, @@ -76,7 +76,6 @@ default boolean isCandidateClass(Class targetClass) { * @return the configured caching operation, or {@code null} if none found * @see AnnotationCacheOperationSource#findCacheOperations(Method) */ - @Nullable - Collection parseCacheAnnotations(Method method); + @Nullable Collection parseCacheAnnotations(Method method); } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java index 26b3afb46a22..1d70fa54a20c 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurer.java @@ -16,11 +16,12 @@ package org.springframework.cache.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.lang.Nullable; /** * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration @@ -66,8 +67,7 @@ public interface CachingConfigurer { * * See @{@link EnableCaching} for more complete examples. */ - @Nullable - default CacheManager cacheManager() { + default @Nullable CacheManager cacheManager() { return null; } @@ -94,8 +94,7 @@ default CacheManager cacheManager() { * * See {@link EnableCaching} for more complete examples. */ - @Nullable - default CacheResolver cacheResolver() { + default @Nullable CacheResolver cacheResolver() { return null; } @@ -105,8 +104,7 @@ default CacheResolver cacheResolver() { * is used. * See @{@link EnableCaching} for more complete examples. */ - @Nullable - default KeyGenerator keyGenerator() { + default @Nullable KeyGenerator keyGenerator() { return null; } @@ -116,8 +114,7 @@ default KeyGenerator keyGenerator() { * is used, which throws the exception back at the client. * See @{@link EnableCaching} for more complete examples. */ - @Nullable - default CacheErrorHandler errorHandler() { + default @Nullable CacheErrorHandler errorHandler() { return null; } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java index 16886ff13f88..33aadd02c33a 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurerSupport.java @@ -16,11 +16,12 @@ package org.springframework.cache.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.CacheManager; import org.springframework.cache.interceptor.CacheErrorHandler; import org.springframework.cache.interceptor.CacheResolver; import org.springframework.cache.interceptor.KeyGenerator; -import org.springframework.lang.Nullable; /** * An implementation of {@link CachingConfigurer} with empty methods allowing @@ -35,26 +36,22 @@ public class CachingConfigurerSupport implements CachingConfigurer { @Override - @Nullable - public CacheManager cacheManager() { + public @Nullable CacheManager cacheManager() { return null; } @Override - @Nullable - public CacheResolver cacheResolver() { + public @Nullable CacheResolver cacheResolver() { return null; } @Override - @Nullable - public KeyGenerator keyGenerator() { + public @Nullable KeyGenerator keyGenerator() { return null; } @Override - @Nullable - public CacheErrorHandler errorHandler() { + public @Nullable CacheErrorHandler errorHandler() { return null; } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java index 49bf01e26d10..5851f46bd0a1 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/SpringCacheAnnotationParser.java @@ -24,13 +24,14 @@ import java.util.Collection; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.interceptor.CacheEvictOperation; import org.springframework.cache.interceptor.CacheOperation; import org.springframework.cache.interceptor.CachePutOperation; import org.springframework.cache.interceptor.CacheableOperation; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -58,21 +59,18 @@ public boolean isCandidateClass(Class targetClass) { } @Override - @Nullable - public Collection parseCacheAnnotations(Class type) { + public @Nullable Collection parseCacheAnnotations(Class type) { DefaultCacheConfig defaultConfig = new DefaultCacheConfig(type); return parseCacheAnnotations(defaultConfig, type); } @Override - @Nullable - public Collection parseCacheAnnotations(Method method) { + public @Nullable Collection parseCacheAnnotations(Method method) { DefaultCacheConfig defaultConfig = new DefaultCacheConfig(method.getDeclaringClass()); return parseCacheAnnotations(defaultConfig, method); } - @Nullable - private Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { + private @Nullable Collection parseCacheAnnotations(DefaultCacheConfig cachingConfig, AnnotatedElement ae) { Collection ops = parseCacheAnnotations(cachingConfig, ae, false); if (ops != null && ops.size() > 1) { // More than one operation found -> local declarations override interface-declared ones... @@ -84,8 +82,7 @@ private Collection parseCacheAnnotations(DefaultCacheConfig cach return ops; } - @Nullable - private Collection parseCacheAnnotations( + private @Nullable Collection parseCacheAnnotations( DefaultCacheConfig cachingConfig, AnnotatedElement ae, boolean localOnly) { Collection annotations = (localOnly ? @@ -232,17 +229,13 @@ private static class DefaultCacheConfig { private final Class target; - @Nullable - private String[] cacheNames; + private String @Nullable [] cacheNames; - @Nullable - private String keyGenerator; + private @Nullable String keyGenerator; - @Nullable - private String cacheManager; + private @Nullable String cacheManager; - @Nullable - private String cacheResolver; + private @Nullable String cacheResolver; private boolean initialized = false; diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java b/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java index 0a53f33ac3b4..3c980e2b019d 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/package-info.java @@ -3,9 +3,7 @@ * Hooked into Spring's cache interception infrastructure via * {@link org.springframework.cache.interceptor.CacheOperationSource}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java index 0facea830e29..d73913e336d4 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCache.java @@ -23,9 +23,10 @@ import java.util.concurrent.ForkJoinPool; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.support.AbstractValueAdaptingCache; import org.springframework.core.serializer.support.SerializationDelegate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -57,8 +58,7 @@ public class ConcurrentMapCache extends AbstractValueAdaptingCache { private final ConcurrentMap store; - @Nullable - private final SerializationDelegate serialization; + private final @Nullable SerializationDelegate serialization; /** @@ -137,15 +137,13 @@ public final ConcurrentMap getNativeCache() { } @Override - @Nullable - protected Object lookup(Object key) { + protected @Nullable Object lookup(Object key) { return this.store.get(key); } @SuppressWarnings("unchecked") @Override - @Nullable - public T get(Object key, Callable valueLoader) { + public @Nullable T get(Object key, Callable valueLoader) { return (T) fromStoreValue(this.store.computeIfAbsent(key, k -> { try { return toStoreValue(valueLoader.call()); @@ -157,8 +155,7 @@ public T get(Object key, Callable valueLoader) { } @Override - @Nullable - public CompletableFuture retrieve(Object key) { + public @Nullable CompletableFuture retrieve(Object key) { Object value = lookup(key); return (value != null ? CompletableFuture.completedFuture( isAllowNullValues() ? toValueWrapper(value) : fromStoreValue(value)) : null); @@ -177,8 +174,7 @@ public void put(Object key, @Nullable Object value) { } @Override - @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + public @Nullable ValueWrapper putIfAbsent(Object key, @Nullable Object value) { Object existing = this.store.putIfAbsent(key, toStoreValue(value)); return toValueWrapper(existing); } @@ -223,8 +219,7 @@ protected Object toStoreValue(@Nullable Object userValue) { } @Override - @Nullable - protected Object fromStoreValue(@Nullable Object storeValue) { + protected @Nullable Object fromStoreValue(@Nullable Object storeValue) { if (storeValue != null && this.serialization != null) { try { return super.fromStoreValue(this.serialization.deserializeFromByteArray((byte[]) storeValue)); diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java index b559e10b3d6d..ad8023f802df 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheFactoryBean.java @@ -18,10 +18,11 @@ import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -42,13 +43,11 @@ public class ConcurrentMapCacheFactoryBean private String name = ""; - @Nullable - private ConcurrentMap store; + private @Nullable ConcurrentMap store; private boolean allowNullValues = true; - @Nullable - private ConcurrentMapCache cache; + private @Nullable ConcurrentMapCache cache; /** @@ -92,8 +91,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public ConcurrentMapCache getObject() { + public @Nullable ConcurrentMapCache getObject() { return this.cache; } diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java index 6dfa5a4eda31..ef4a26936068 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/ConcurrentMapCacheManager.java @@ -24,11 +24,12 @@ import java.util.concurrent.ConcurrentMap; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.core.serializer.support.SerializationDelegate; -import org.springframework.lang.Nullable; /** * {@link CacheManager} implementation that lazily builds {@link ConcurrentMapCache} @@ -60,8 +61,7 @@ public class ConcurrentMapCacheManager implements CacheManager, BeanClassLoaderA private boolean storeByValue = false; - @Nullable - private SerializationDelegate serialization; + private @Nullable SerializationDelegate serialization; /** @@ -166,8 +166,7 @@ public Collection getCacheNames() { } @Override - @Nullable - public Cache getCache(String name) { + public @Nullable Cache getCache(String name) { Cache cache = this.cacheMap.get(name); if (cache == null && this.dynamic) { cache = this.cacheMap.computeIfAbsent(name, this::createConcurrentMapCache); diff --git a/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java b/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java index 5c5feb58e60e..fd7f64befab9 100644 --- a/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/concurrent/package-info.java @@ -4,9 +4,7 @@ * and {@link org.springframework.cache.Cache Cache} implementation for * use in a Spring context, using a JDK based thread pool at runtime. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.concurrent; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java index f2768059a0db..c2e12c08eeaa 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java @@ -16,6 +16,7 @@ package org.springframework.cache.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.aop.config.AopNamespaceUtils; @@ -28,7 +29,6 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor; import org.springframework.cache.interceptor.CacheInterceptor; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -79,8 +79,7 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser * register an AutoProxyCreator} with the container as necessary. */ @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { String mode = element.getAttribute("mode"); if ("aspectj".equals(mode)) { // mode="aspectj" diff --git a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java index c85dcad9065d..23a260609d9a 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/CacheAdviceParser.java @@ -20,6 +20,7 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.TypedStringValue; @@ -36,7 +37,6 @@ import org.springframework.cache.interceptor.CachePutOperation; import org.springframework.cache.interceptor.CacheableOperation; import org.springframework.cache.interceptor.NameMatchCacheOperationSource; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -185,8 +185,7 @@ private static class Props { private final String method; - @Nullable - private String[] caches; + private String @Nullable [] caches; Props(Element root) { String defaultCache = root.getAttribute("cache"); @@ -231,8 +230,7 @@ T merge(Element element, ReaderContext reader return builder; } - @Nullable - String merge(Element element, ReaderContext readerCtx) { + @Nullable String merge(Element element, ReaderContext readerCtx) { String method = element.getAttribute(METHOD_ATTRIBUTE); if (StringUtils.hasText(method)) { return method.trim(); diff --git a/spring-context/src/main/java/org/springframework/cache/config/package-info.java b/spring-context/src/main/java/org/springframework/cache/config/package-info.java index b26305693b33..39e10f5ebdba 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/config/package-info.java @@ -4,9 +4,7 @@ * org.springframework.cache.annotation.EnableCaching EnableCaching} * for details on code-based configuration without XML. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 84e36757d61c..5b3fe916e17f 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. @@ -20,8 +20,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; import org.springframework.util.function.SingletonSupplier; /** @@ -72,8 +73,7 @@ public CacheErrorHandler getErrorHandler() { * miss in case of error. * @see Cache#get(Object) */ - @Nullable - protected Cache.ValueWrapper doGet(Cache cache, Object key) { + protected Cache.@Nullable ValueWrapper doGet(Cache cache, Object key) { try { return cache.get(key); } @@ -91,8 +91,7 @@ protected Cache.ValueWrapper doGet(Cache cache, Object key) { * @since 6.2 * @see Cache#get(Object, Callable) */ - @Nullable - protected T doGet(Cache cache, Object key, Callable valueLoader) { + protected @Nullable T doGet(Cache cache, Object key, Callable valueLoader) { try { return cache.get(key, valueLoader); } @@ -105,7 +104,7 @@ protected T doGet(Cache cache, Object key, Callable valueLoader) { return valueLoader.call(); } catch (Exception ex2) { - throw new RuntimeException(ex2); + throw new Cache.ValueRetrievalException(key, valueLoader, ex); } } } @@ -119,21 +118,16 @@ protected T doGet(Cache cache, Object key, Callable valueLoader) { * @since 6.2 * @see Cache#retrieve(Object) */ - @Nullable - protected CompletableFuture doRetrieve(Cache cache, Object key) { + protected @Nullable CompletableFuture doRetrieve(Cache cache, Object key) { 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. @@ -146,9 +140,6 @@ protected CompletableFuture doRetrieve(Cache cache, Object key, Supplier< 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/AbstractCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java index a54ed05f17a1..4cb9d228a4b5 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractCacheResolver.java @@ -20,10 +20,11 @@ import java.util.Collection; import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ */ public abstract class AbstractCacheResolver implements CacheResolver, InitializingBean { - @Nullable - private CacheManager cacheManager; + private @Nullable CacheManager cacheManager; /** @@ -103,7 +103,6 @@ public Collection resolveCaches(CacheOperationInvocationContext * @param context the context of the particular invocation * @return the cache name(s) to resolve, or {@code null} if no cache should be resolved */ - @Nullable - protected abstract Collection getCacheNames(CacheOperationInvocationContext context); + protected abstract @Nullable Collection getCacheNames(CacheOperationInvocationContext context); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index 2657232886c5..7928cdfd9c06 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -25,10 +25,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; import org.springframework.core.MethodClassKey; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -78,8 +78,7 @@ public boolean hasCacheOperations(Method method, @Nullable Class targetClass) } @Override - @Nullable - public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + public @Nullable Collection getCacheOperations(Method method, @Nullable Class targetClass) { return getCacheOperations(method, targetClass, true); } @@ -92,8 +91,7 @@ public Collection getCacheOperations(Method method, @Nullable Cl * @return {@link CacheOperation} for this method, or {@code null} if the method * is not cacheable */ - @Nullable - private Collection getCacheOperations( + private @Nullable Collection getCacheOperations( Method method, @Nullable Class targetClass, boolean cacheNull) { if (ReflectionUtils.isObjectMethod(method)) { @@ -133,8 +131,7 @@ protected Object getCacheKey(Method method, @Nullable Class targetClass) { return new MethodClassKey(method, targetClass); } - @Nullable - private Collection computeCacheOperations(Method method, @Nullable Class targetClass) { + private @Nullable Collection computeCacheOperations(Method method, @Nullable Class targetClass) { // Don't allow non-public methods, as configured. if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { return null; @@ -179,8 +176,7 @@ private Collection computeCacheOperations(Method method, @Nullab * @param clazz the class to retrieve the cache operations for * @return all cache operations associated with this class, or {@code null} if none */ - @Nullable - protected abstract Collection findCacheOperations(Class clazz); + protected abstract @Nullable Collection findCacheOperations(Class clazz); /** * Subclasses need to implement this to return the cache operations for the @@ -188,8 +184,7 @@ private Collection computeCacheOperations(Method method, @Nullab * @param method the method to retrieve the cache operations for * @return all cache operations associated with this method, or {@code null} if none */ - @Nullable - protected abstract Collection findCacheOperations(Method method); + protected abstract @Nullable Collection findCacheOperations(Method method); /** * Should only public methods be allowed to have caching semantics? 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 8f97d45f70dd..876f77279789 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 @@ -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,10 +26,12 @@ 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; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.publisher.Flux; @@ -55,7 +57,6 @@ import org.springframework.core.SpringProperties; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -129,19 +130,15 @@ public abstract class CacheAspectSupport extends AbstractCacheInvoker private final CacheOperationExpressionEvaluator evaluator = new CacheOperationExpressionEvaluator( new CacheEvaluationContextFactory(this.originalEvaluationContext)); - @Nullable - private final ReactiveCachingHandler reactiveCachingHandler; + private final @Nullable ReactiveCachingHandler reactiveCachingHandler; - @Nullable - private CacheOperationSource cacheOperationSource; + private @Nullable CacheOperationSource cacheOperationSource; private SingletonSupplier keyGenerator = SingletonSupplier.of(SimpleKeyGenerator::new); - @Nullable - private SingletonSupplier cacheResolver; + private @Nullable SingletonSupplier cacheResolver; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; private boolean initialized = false; @@ -158,8 +155,8 @@ protected CacheAspectSupport() { * @since 5.1 */ public void configure( - @Nullable Supplier errorHandler, @Nullable Supplier keyGenerator, - @Nullable Supplier cacheResolver, @Nullable Supplier cacheManager) { + @Nullable Supplier errorHandler, @Nullable Supplier keyGenerator, + @Nullable Supplier cacheResolver, @Nullable Supplier cacheManager) { this.errorHandler = new SingletonSupplier<>(errorHandler, SimpleCacheErrorHandler::new); this.keyGenerator = new SingletonSupplier<>(keyGenerator, SimpleKeyGenerator::new); @@ -192,8 +189,7 @@ public void setCacheOperationSource(@Nullable CacheOperationSource cacheOperatio /** * Return the CacheOperationSource for this cache aspect. */ - @Nullable - public CacheOperationSource getCacheOperationSource() { + public @Nullable CacheOperationSource getCacheOperationSource() { return this.cacheOperationSource; } @@ -228,8 +224,7 @@ public void setCacheResolver(@Nullable CacheResolver cacheResolver) { /** * Return the default {@link CacheResolver} that this cache aspect delegates to. */ - @Nullable - public CacheResolver getCacheResolver() { + public @Nullable CacheResolver getCacheResolver() { return SupplierUtils.resolve(this.cacheResolver); } @@ -288,8 +283,8 @@ public void afterSingletonsInstantiated() { } } catch (NoSuchBeanDefinitionException ex) { - throw new NoSuchBeanDefinitionException(CacheManager.class, "no CacheResolver specified - " - + "register a CacheManager bean or remove the @EnableCaching annotation from your configuration."); + throw new NoSuchBeanDefinitionException(CacheManager.class, "no CacheResolver specified - " + + "register a CacheManager bean or remove the @EnableCaching annotation from your configuration."); } } this.initialized = true; @@ -323,7 +318,7 @@ protected Collection getCaches( } protected CacheOperationContext getOperationContext( - CacheOperation operation, Method method, Object[] args, Object target, Class targetClass) { + CacheOperation operation, Method method, @Nullable Object[] args, Object target, Class targetClass) { CacheOperationMetadata metadata = getCacheOperationMetadata(operation, method, targetClass); return new CacheOperationContext(metadata, args, target); @@ -397,8 +392,7 @@ protected void clearMetadataCache() { this.evaluator.clear(); } - @Nullable - protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { + protected @Nullable Object execute(CacheOperationInvoker invoker, Object target, Method method, @Nullable Object[] args) { // Check whether aspect is enabled (to cope with cases where the AJ is pulled in automatically) if (this.initialized) { Class targetClass = AopProxyUtils.ultimateTargetClass(target); @@ -425,13 +419,11 @@ protected Object execute(CacheOperationInvoker invoker, Object target, Method me * @return the result of the invocation * @see CacheOperationInvoker#invoke() */ - @Nullable - protected Object invokeOperation(CacheOperationInvoker invoker) { + protected @Nullable Object invokeOperation(CacheOperationInvoker invoker) { return invoker.invoke(); } - @Nullable - private Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + private @Nullable Object execute(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { if (contexts.isSynchronized()) { // Special handling of synchronized invocation return executeSynchronized(invoker, method, contexts); @@ -449,14 +441,40 @@ private Object execute(CacheOperationInvoker invoker, Method method, CacheOperat return cacheHit; } - @Nullable - private Object executeSynchronized(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + @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); @@ -487,8 +505,7 @@ private Object executeSynchronized(CacheOperationInvoker invoker, Method method, * @return a {@link Cache.ValueWrapper} holding the cached value, * or {@code null} if none is found */ - @Nullable - private Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { + private @Nullable Object findCachedValue(CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { for (CacheOperationContext context : contexts.get(CacheableOperation.class)) { if (isConditionPassing(context, CacheOperationExpressionEvaluator.NO_RESULT)) { Object key = generateKey(context, CacheOperationExpressionEvaluator.NO_RESULT); @@ -509,17 +526,24 @@ private Object findCachedValue(CacheOperationInvoker invoker, Method method, Cac return null; } - @Nullable - private Object findInCaches(CacheOperationContext context, Object key, + private @Nullable Object findInCaches(CacheOperationContext context, Object key, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { for (Cache cache : context.getCaches()) { 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)); @@ -543,8 +567,7 @@ private Object findInCaches(CacheOperationContext context, Object key, return null; } - @Nullable - private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method, + private @Nullable Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { // Re-invocation in reactive pipeline after late cache hit determination? @@ -596,13 +619,11 @@ private Object evaluate(@Nullable Object cacheHit, CacheOperationInvoker invoker return returnValue; } - @Nullable - private Object unwrapCacheValue(@Nullable Object cacheValue) { + private @Nullable Object unwrapCacheValue(@Nullable Object cacheValue) { return (cacheValue instanceof Cache.ValueWrapper wrapper ? wrapper.get() : cacheValue); } - @Nullable - private Object wrapCacheValue(Method method, @Nullable Object cacheValue) { + private @Nullable Object wrapCacheValue(Method method, @Nullable Object cacheValue) { if (method.getReturnType() == Optional.class && (cacheValue == null || cacheValue.getClass() != Optional.class)) { return Optional.ofNullable(cacheValue); @@ -610,8 +631,7 @@ private Object wrapCacheValue(Method method, @Nullable Object cacheValue) { return cacheValue; } - @Nullable - private Object unwrapReturnValue(@Nullable Object returnValue) { + private @Nullable Object unwrapReturnValue(@Nullable Object returnValue) { return ObjectUtils.unwrapOptional(returnValue); } @@ -633,8 +653,7 @@ private boolean hasCachePut(CacheOperationContexts contexts) { return (cachePutContexts.size() != excluded.size()); } - @Nullable - private Object processCacheEvicts(Collection contexts, boolean beforeInvocation, + private @Nullable Object processCacheEvicts(Collection contexts, boolean beforeInvocation, @Nullable Object result) { if (contexts.isEmpty()) { @@ -743,7 +762,7 @@ private class CacheOperationContexts { boolean processed; public CacheOperationContexts(Collection operations, Method method, - Object[] args, Object target, Class targetClass) { + @Nullable Object[] args, Object target, Class targetClass) { this.contexts = new LinkedMultiValueMap<>(operations.size()); for (CacheOperation op : operations) { @@ -841,7 +860,7 @@ protected class CacheOperationContext implements CacheOperationInvocationContext private final CacheOperationMetadata metadata; - private final Object[] args; + private final @Nullable Object[] args; private final Object target; @@ -849,13 +868,11 @@ protected class CacheOperationContext implements CacheOperationInvocationContext private final Collection cacheNames; - @Nullable - private Boolean conditionPassing; + private @Nullable Boolean conditionPassing; - @Nullable - private Object key; + private @Nullable Object key; - public CacheOperationContext(CacheOperationMetadata metadata, Object[] args, Object target) { + public CacheOperationContext(CacheOperationMetadata metadata, @Nullable Object[] args, Object target) { this.metadata = metadata; this.args = extractArgs(metadata.method, args); this.target = target; @@ -879,11 +896,11 @@ public Method getMethod() { } @Override - public Object[] getArgs() { + public @Nullable Object[] getArgs() { return this.args; } - private Object[] extractArgs(Method method, Object[] args) { + private @Nullable Object[] extractArgs(Method method, @Nullable Object[] args) { if (!method.isVarArgs()) { return args; } @@ -926,8 +943,7 @@ else if (this.metadata.operation instanceof CachePutOperation cachePutOperation) /** * Compute the key for the given caching operation. */ - @Nullable - protected Object generateKey(@Nullable Object result) { + protected @Nullable Object generateKey(@Nullable Object result) { if (StringUtils.hasText(this.metadata.operation.getKey())) { EvaluationContext evaluationContext = createEvaluationContext(result); this.key = evaluator.key(this.metadata.operation.getKey(), this.metadata.methodKey, evaluationContext); @@ -943,8 +959,7 @@ protected Object generateKey(@Nullable Object result) { * @return generated key * @since 6.1.2 */ - @Nullable - protected Object getGeneratedKey() { + protected @Nullable Object getGeneratedKey() { return this.key; } @@ -1018,8 +1033,7 @@ public CachePutRequest(CacheOperationContext context) { this.context = context; } - @Nullable - public Object apply(@Nullable Object result) { + public @Nullable Object apply(@Nullable Object result) { if (result instanceof CompletableFuture future) { return future.whenComplete((value, ex) -> { if (ex == null) { @@ -1097,38 +1111,76 @@ private class ReactiveCachingHandler { private final ReactiveAdapterRegistry registry = ReactiveAdapterRegistry.getSharedInstance(); - @Nullable - public Object executeSynchronized(CacheOperationInvoker invoker, Method method, Cache cache, Object key) { + @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.isKotlinReflectPresent() && KotlinDetector.isSuspendingFunction(method)) { - return Mono.fromFuture(cache.retrieve(key, () -> { - Mono mono = ((Mono) invokeOperation(invoker)); - if (mono == null) { + if (KotlinDetector.isSuspendingFunction(method)) { + 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; } - @Nullable - public Object processCacheEvicts(List contexts, @Nullable Object result) { + public @Nullable Object processCacheEvicts(List contexts, @Nullable Object result) { ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null); if (adapter != null) { return adapter.fromPublisher(Mono.from(adapter.toPublisher(result)) @@ -1137,9 +1189,8 @@ public Object processCacheEvicts(List contexts, @Nullable return NOT_HANDLED; } - @SuppressWarnings({ "unchecked", "rawtypes" }) - @Nullable - public Object findInCaches(CacheOperationContext context, Cache cache, Object key, + @SuppressWarnings({"rawtypes", "unchecked"}) + public @Nullable Object findInCaches(CacheOperationContext context, Cache cache, Object key, CacheOperationInvoker invoker, Method method, CacheOperationContexts contexts) { ReactiveAdapter adapter = this.registry.getAdapter(context.getMethod().getReturnType()); @@ -1188,8 +1239,7 @@ private Flux valueToFlux(Object value, CacheOperationContexts contexts) { (data != null ? Flux.just(data) : Flux.empty())); } - @Nullable - public Object processPutRequest(CachePutRequest request, @Nullable Object result) { + public @Nullable Object processPutRequest(CachePutRequest request, @Nullable Object result) { ReactiveAdapter adapter = (result != null ? this.registry.getAdapter(result.getClass()) : null); if (adapter != null) { if (adapter.isMultiValue()) { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java index a31ad42731e1..20b33f5e0b97 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheErrorHandler.java @@ -16,8 +16,9 @@ package org.springframework.cache.interceptor; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; /** * A strategy for handling cache-related errors. In most cases, any diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java index 29604d91b648..d00f8cf903bf 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContext.java @@ -20,9 +20,10 @@ import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.context.expression.MethodBasedEvaluationContext; import org.springframework.core.ParameterNameDiscoverer; -import org.springframework.lang.Nullable; /** * Cache-specific evaluation context that adds method parameters as SpEL @@ -47,7 +48,7 @@ class CacheEvaluationContext extends MethodBasedEvaluationContext { private final Set unavailableVariables = new HashSet<>(1); - CacheEvaluationContext(Object rootObject, Method method, Object[] arguments, + CacheEvaluationContext(Object rootObject, Method method, @Nullable Object[] arguments, ParameterNameDiscoverer parameterNameDiscoverer) { super(rootObject, method, arguments, parameterNameDiscoverer); @@ -70,8 +71,7 @@ public void addUnavailableVariable(String name) { * Load the param information only when needed. */ @Override - @Nullable - public Object lookupVariable(String name) { + public @Nullable Object lookupVariable(String name) { if (this.unavailableVariables.contains(name)) { throw new VariableNotAvailableException(name); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java index 327cfd4d9768..d417f4b80be6 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheEvaluationContextFactory.java @@ -19,10 +19,11 @@ import java.lang.reflect.Method; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.function.SingletonSupplier; /** @@ -36,8 +37,7 @@ class CacheEvaluationContextFactory { private final StandardEvaluationContext originalContext; - @Nullable - private Supplier parameterNameDiscoverer; + private @Nullable Supplier parameterNameDiscoverer; CacheEvaluationContextFactory(StandardEvaluationContext originalContext) { this.originalContext = originalContext; @@ -62,7 +62,7 @@ public ParameterNameDiscoverer getParameterNameDiscoverer() { * @return a context suitable for this cache operation */ public CacheEvaluationContext forOperation(CacheExpressionRootObject rootObject, - Method targetMethod, Object[] args) { + Method targetMethod, @Nullable Object[] args) { CacheEvaluationContext evaluationContext = new CacheEvaluationContext( rootObject, targetMethod, args, getParameterNameDiscoverer()); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java index 32f55cf77500..7fc9db8786a5 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheExpressionRootObject.java @@ -19,6 +19,8 @@ import java.lang.reflect.Method; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; /** @@ -34,7 +36,7 @@ class CacheExpressionRootObject { private final Method method; - private final Object[] args; + private final @Nullable Object[] args; private final Object target; @@ -42,7 +44,7 @@ class CacheExpressionRootObject { public CacheExpressionRootObject( - Collection caches, Method method, Object[] args, Object target, Class targetClass) { + Collection caches, Method method, @Nullable Object[] args, Object target, Class targetClass) { this.method = method; this.target = target; @@ -64,7 +66,7 @@ public String getMethodName() { return this.method.getName(); } - public Object[] getArgs() { + public @Nullable Object[] getArgs() { return this.args; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java index 2f1f56f14a7d..cc06a5e275bc 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java @@ -21,8 +21,8 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -46,8 +46,7 @@ public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor, Serializable { @Override - @Nullable - public Object invoke(final MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); CacheOperationInvoker aopAllianceInvoker = () -> { diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java index a906e895b323..52f2a50402f6 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperation.java @@ -19,7 +19,8 @@ import java.util.Collections; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java index 13a49ea1026b..9a68c438bd98 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluator.java @@ -21,12 +21,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.context.expression.CachedExpressionEvaluator; import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; -import org.springframework.lang.Nullable; /** * Utility class handling the SpEL expression parsing. @@ -85,7 +86,7 @@ public CacheOperationExpressionEvaluator(CacheEvaluationContextFactory evaluatio * @return the evaluation context */ public EvaluationContext createEvaluationContext(Collection caches, - Method method, Object[] args, Object target, Class targetClass, Method targetMethod, + Method method, @Nullable Object[] args, Object target, Class targetClass, Method targetMethod, @Nullable Object result) { CacheExpressionRootObject rootObject = new CacheExpressionRootObject( @@ -101,8 +102,7 @@ else if (result != NO_RESULT) { return evaluationContext; } - @Nullable - public Object key(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { + public @Nullable Object key(String keyExpression, AnnotatedElementKey methodKey, EvaluationContext evalContext) { return getExpression(this.keyCache, methodKey, keyExpression).getValue(evalContext); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java index c459a09aea6d..6db02e3c9791 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + /** * Representation of the context of the invocation of a cache operation. * @@ -48,6 +50,6 @@ public interface CacheOperationInvocationContext { /** * Return the argument list used to invoke the method. */ - Object[] getArgs(); + @Nullable Object[] getArgs(); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java index e37736480e51..00144c97b2ef 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -16,7 +16,7 @@ package org.springframework.cache.interceptor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract the invocation of a cache operation. @@ -38,8 +38,7 @@ public interface CacheOperationInvoker { * @return the result of the operation * @throws ThrowableWrapper if an error occurred while invoking the operation */ - @Nullable - Object invoke() throws ThrowableWrapper; + @Nullable Object invoke() throws ThrowableWrapper; /** diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java index 601fd4dbb4c1..6a923fcdeec6 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSource.java @@ -19,7 +19,8 @@ import java.lang.reflect.Method; import java.util.Collection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.CollectionUtils; /** @@ -72,7 +73,6 @@ default boolean hasCacheOperations(Method method, @Nullable Class targetClass * the declaring class of the method must be used) * @return all cache operations for this method, or {@code null} if none found */ - @Nullable - Collection getCacheOperations(Method method, @Nullable Class targetClass); + @Nullable Collection getCacheOperations(Method method, @Nullable Class targetClass); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java index 4b9054b10edb..a90daa661674 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationSourcePointcut.java @@ -19,10 +19,11 @@ import java.io.Serializable; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.ClassFilter; import org.springframework.aop.support.StaticMethodMatcherPointcut; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -37,8 +38,7 @@ @SuppressWarnings("serial") final class CacheOperationSourcePointcut extends StaticMethodMatcherPointcut implements Serializable { - @Nullable - private CacheOperationSource cacheOperationSource; + private @Nullable CacheOperationSource cacheOperationSource; public CacheOperationSourcePointcut() { @@ -87,8 +87,7 @@ public boolean matches(Class clazz) { return (cacheOperationSource == null || cacheOperationSource.isCandidateClass(clazz)); } - @Nullable - private CacheOperationSource getCacheOperationSource() { + private @Nullable CacheOperationSource getCacheOperationSource() { return cacheOperationSource; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java index 4b94ac5edff5..29e1948acce1 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CachePutOperation.java @@ -16,7 +16,7 @@ package org.springframework.cache.interceptor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Class describing a cache 'put' operation. @@ -28,8 +28,7 @@ */ public class CachePutOperation extends CacheOperation { - @Nullable - private final String unless; + private final @Nullable String unless; /** @@ -42,8 +41,7 @@ public CachePutOperation(CachePutOperation.Builder b) { } - @Nullable - public String getUnless() { + public @Nullable String getUnless() { return this.unless; } @@ -54,8 +52,7 @@ public String getUnless() { */ public static class Builder extends CacheOperation.Builder { - @Nullable - private String unless; + private @Nullable String unless; public void setUnless(String unless) { this.unless = unless; diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java index 9f7fcc2e97b0..02a71682c789 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheableOperation.java @@ -16,7 +16,7 @@ package org.springframework.cache.interceptor; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Class describing a cache 'cacheable' operation. @@ -28,8 +28,7 @@ */ public class CacheableOperation extends CacheOperation { - @Nullable - private final String unless; + private final @Nullable String unless; private final boolean sync; @@ -45,8 +44,7 @@ public CacheableOperation(CacheableOperation.Builder b) { } - @Nullable - public String getUnless() { + public @Nullable String getUnless() { return this.unless; } @@ -61,8 +59,7 @@ public boolean isSync() { */ public static class Builder extends CacheOperation.Builder { - @Nullable - private String unless; + private @Nullable String unless; private boolean sync; diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java index 9e1387a9ca04..b54d75083d74 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CompositeCacheOperationSource.java @@ -21,7 +21,8 @@ import java.util.ArrayList; import java.util.Collection; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -77,8 +78,7 @@ public boolean hasCacheOperations(Method method, @Nullable Class targetClass) } @Override - @Nullable - public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + public @Nullable Collection getCacheOperations(Method method, @Nullable Class targetClass) { Collection ops = null; for (CacheOperationSource source : this.cacheOperationSources) { Collection cacheOperations = source.getCacheOperations(method, targetClass); diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java index 2d99e4994d99..a767b339f3e8 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/KeyGenerator.java @@ -18,6 +18,8 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + /** * Cache key generator. Used for creating a key based on the given method * (used as context) and its parameters. @@ -37,6 +39,6 @@ public interface KeyGenerator { * @param params the method parameters (with any var-args expanded) * @return a generated key */ - Object generate(Object target, Method method, Object... params); + Object generate(Object target, Method method, @Nullable Object... params); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/LoggingCacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/LoggingCacheErrorHandler.java index a0d2a4ccd7b3..5f71eb761247 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/LoggingCacheErrorHandler.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/LoggingCacheErrorHandler.java @@ -20,9 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java index 67644f5e92a2..c72aa89ef2b6 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NameMatchCacheOperationSource.java @@ -24,8 +24,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -75,8 +75,7 @@ public void addCacheMethod(String methodName, Collection ops) { } @Override - @Nullable - public Collection getCacheOperations(Method method, @Nullable Class targetClass) { + public @Nullable Collection getCacheOperations(Method method, @Nullable Class targetClass) { // look for direct name match String methodName = method.getName(); Collection ops = this.nameMap.get(methodName); @@ -85,8 +84,8 @@ public Collection getCacheOperations(Method method, @Nullable Cl // Look for most specific name match. String bestNameMatch = null; for (String mappedName : this.nameMap.keySet()) { - if (isMatch(methodName, mappedName) - && (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) { + if (isMatch(methodName, mappedName) && + (bestNameMatch == null || bestNameMatch.length() <= mappedName.length())) { ops = this.nameMap.get(mappedName); bestNameMatch = mappedName; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java index 61cfc4b469db..71b681ba278c 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/NamedCacheResolver.java @@ -19,8 +19,9 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; /** * A {@link CacheResolver} that forces the resolution to a configurable @@ -31,8 +32,7 @@ */ public class NamedCacheResolver extends AbstractCacheResolver { - @Nullable - private Collection cacheNames; + private @Nullable Collection cacheNames; public NamedCacheResolver() { @@ -52,8 +52,7 @@ public void setCacheNames(Collection cacheNames) { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { return this.cacheNames; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java index 99ba25237102..9c3dc0b6de9b 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheErrorHandler.java @@ -16,8 +16,9 @@ package org.springframework.cache.interceptor; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; /** * A simple {@link CacheErrorHandler} that does not handle the diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java index 3c22f3e328d9..ef61ca339407 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.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. @@ -18,9 +18,11 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; /** * A simple {@link CacheResolver} that resolves the {@link Cache} instance(s) @@ -62,8 +64,8 @@ protected Collection getCacheNames(CacheOperationInvocationContext co * @return the SimpleCacheResolver ({@code null} if the CacheManager was {@code null}) * @since 5.1 */ - @Nullable - static SimpleCacheResolver of(@Nullable CacheManager cacheManager) { + @Contract("null -> null; !null -> !null") + static @Nullable SimpleCacheResolver of(@Nullable CacheManager cacheManager) { return (cacheManager != null ? new SimpleCacheResolver(cacheManager) : null); } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java index d0f2a64ce82c..df8055ded476 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.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.io.Serializable; import java.util.Arrays; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -29,6 +30,7 @@ * * @author Phillip Webb * @author Juergen Hoeller + * @author Brian Clozel * @since 4.0 * @see SimpleKeyGenerator */ @@ -41,7 +43,7 @@ public class SimpleKey implements Serializable { public static final SimpleKey EMPTY = new SimpleKey(); - private final Object[] params; + private final @Nullable Object[] params; // Effectively final, just re-calculated on deserialization private transient int hashCode; @@ -51,11 +53,11 @@ public class SimpleKey implements Serializable { * Create a new {@link SimpleKey} instance. * @param elements the elements of the key */ - public SimpleKey(Object... elements) { + public SimpleKey(@Nullable Object... elements) { Assert.notNull(elements, "Elements must not be null"); this.params = elements.clone(); // Pre-calculate hashCode field - this.hashCode = Arrays.deepHashCode(this.params); + this.hashCode = calculateHash(this.params); } @@ -78,7 +80,18 @@ public String toString() { private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException { ois.defaultReadObject(); // Re-calculate hashCode field on deserialization - this.hashCode = Arrays.deepHashCode(this.params); + this.hashCode = calculateHash(this.params); + } + + /** + * Calculate the hash of the key using its elements and + * mix the result with the finalising function of MurmurHash3. + */ + private static int calculateHash(@Nullable Object[] params) { + int hash = Arrays.deepHashCode(params); + hash = (hash ^ (hash >>> 16)) * 0x85ebca6b; + hash = (hash ^ (hash >>> 13)) * 0xc2b2ae35; + return hash ^ (hash >>> 16); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java index c2365ad9e651..b9df9ca657b9 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKeyGenerator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.lang.reflect.Method; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.core.KotlinDetector; /** @@ -41,7 +43,7 @@ public class SimpleKeyGenerator implements KeyGenerator { @Override - public Object generate(Object target, Method method, Object... params) { + public Object generate(Object target, Method method, @Nullable Object... params) { return generateKey((KotlinDetector.isSuspendingFunction(method) ? Arrays.copyOf(params, params.length - 1) : params)); } @@ -49,7 +51,7 @@ public Object generate(Object target, Method method, Object... params) { /** * Generate a key based on the specified parameters. */ - public static Object generateKey(Object... params) { + public static Object generateKey(@Nullable Object... params) { if (params.length == 0) { return SimpleKey.EMPTY; } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java b/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java index 97810d21f6d7..6ec6cba01bfd 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/package-info.java @@ -3,9 +3,7 @@ * Builds on the AOP infrastructure in org.springframework.aop.framework. * Any POJO can be cache-advised with Spring. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.interceptor; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/cache/package-info.java b/spring-context/src/main/java/org/springframework/cache/package-info.java index dbb69eaa2ffc..cd14eb950cd4 100644 --- a/spring-context/src/main/java/org/springframework/cache/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/package-info.java @@ -2,9 +2,7 @@ * Spring's generic cache abstraction. * Concrete implementations are provided in the subpackages. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java index d1e7decdd933..b0eab7674e1b 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractCacheManager.java @@ -23,10 +23,11 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -86,8 +87,7 @@ public void initializeCaches() { // Lazy cache initialization on access @Override - @Nullable - public Cache getCache(String name) { + public @Nullable Cache getCache(String name) { // Quick check for existing cache... Cache cache = this.cacheMap.get(name); if (cache != null) { @@ -128,8 +128,7 @@ public Collection getCacheNames() { * @see #getCache(String) * @see #getMissingCache(String) */ - @Nullable - protected final Cache lookupCache(String name) { + protected final @Nullable Cache lookupCache(String name) { return this.cacheMap.get(name); } @@ -172,8 +171,7 @@ protected Cache decorateCache(Cache cache) { * @since 4.1 * @see #getCache(String) */ - @Nullable - protected Cache getMissingCache(String name) { + protected @Nullable Cache getMissingCache(String name) { return null; } diff --git a/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java b/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java index e9a5d4f08118..e5012b54226b 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java +++ b/spring-context/src/main/java/org/springframework/cache/support/AbstractValueAdaptingCache.java @@ -16,8 +16,9 @@ package org.springframework.cache.support; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; /** * Common base class for {@link Cache} implementations that need to adapt @@ -53,15 +54,13 @@ public final boolean isAllowNullValues() { } @Override - @Nullable - public ValueWrapper get(Object key) { + public @Nullable ValueWrapper get(Object key) { return toValueWrapper(lookup(key)); } @Override @SuppressWarnings("unchecked") - @Nullable - public T get(Object key, @Nullable Class type) { + public @Nullable T get(Object key, @Nullable Class type) { Object value = fromStoreValue(lookup(key)); if (value != null && type != null && !type.isInstance(value)) { throw new IllegalStateException( @@ -75,8 +74,7 @@ public T get(Object key, @Nullable Class type) { * @param key the key whose associated value is to be returned * @return the raw store value for the key, or {@code null} if none */ - @Nullable - protected abstract Object lookup(Object key); + protected abstract @Nullable Object lookup(Object key); /** @@ -85,8 +83,7 @@ public T get(Object key, @Nullable Class type) { * @param storeValue the store value * @return the value to return to the user */ - @Nullable - protected Object fromStoreValue(@Nullable Object storeValue) { + protected @Nullable Object fromStoreValue(@Nullable Object storeValue) { if (this.allowNullValues && storeValue == NullValue.INSTANCE) { return null; } @@ -117,8 +114,7 @@ protected Object toStoreValue(@Nullable Object userValue) { * @param storeValue the original value * @return the wrapped value */ - @Nullable - protected Cache.ValueWrapper toValueWrapper(@Nullable Object storeValue) { + protected Cache.@Nullable ValueWrapper toValueWrapper(@Nullable Object storeValue) { return (storeValue != null ? new SimpleValueWrapper(fromStoreValue(storeValue)) : null); } diff --git a/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java index 25f0bf12adaa..f4f051a7f2a8 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/CompositeCacheManager.java @@ -24,10 +24,11 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; /** * Composite {@link CacheManager} implementation that iterates over @@ -99,8 +100,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public Cache getCache(String name) { + public @Nullable Cache getCache(String name) { for (CacheManager cacheManager : this.cacheManagers) { Cache cache = cacheManager.getCache(name); if (cache != null) { diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java index b8746e97f91a..115eedb5e8ff 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCache.java @@ -20,8 +20,9 @@ import java.util.concurrent.CompletableFuture; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -60,20 +61,17 @@ public Object getNativeCache() { } @Override - @Nullable - public ValueWrapper get(Object key) { + public @Nullable ValueWrapper get(Object key) { return null; } @Override - @Nullable - public T get(Object key, @Nullable Class type) { + public @Nullable T get(Object key, @Nullable Class type) { return null; } @Override - @Nullable - public T get(Object key, Callable valueLoader) { + public @Nullable T get(Object key, Callable valueLoader) { try { return valueLoader.call(); } @@ -83,8 +81,7 @@ public T get(Object key, Callable valueLoader) { } @Override - @Nullable - public CompletableFuture retrieve(Object key) { + public @Nullable CompletableFuture retrieve(Object key) { return null; } @@ -98,8 +95,7 @@ public void put(Object key, @Nullable Object value) { } @Override - @Nullable - public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { + public @Nullable ValueWrapper putIfAbsent(Object key, @Nullable Object value) { return null; } diff --git a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java index 58d914717dd9..ff2e8bbabdc3 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NoOpCacheManager.java @@ -23,9 +23,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; -import org.springframework.lang.Nullable; /** * A basic, no operation {@link CacheManager} implementation suitable @@ -51,8 +52,7 @@ public class NoOpCacheManager implements CacheManager { * Additionally, the request cache will be remembered by the manager for consistency. */ @Override - @Nullable - public Cache getCache(String name) { + public @Nullable Cache getCache(String name) { Cache cache = this.caches.get(name); if (cache == null) { this.caches.computeIfAbsent(name, NoOpCache::new); diff --git a/spring-context/src/main/java/org/springframework/cache/support/NullValue.java b/spring-context/src/main/java/org/springframework/cache/support/NullValue.java index cc60aa47f8ca..01938a06d374 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/NullValue.java +++ b/spring-context/src/main/java/org/springframework/cache/support/NullValue.java @@ -18,7 +18,7 @@ import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple serializable class that serves as a {@code null} replacement diff --git a/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java b/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java index 460d27a049c3..0eb7c3c52518 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java +++ b/spring-context/src/main/java/org/springframework/cache/support/SimpleValueWrapper.java @@ -18,8 +18,9 @@ import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.cache.Cache.ValueWrapper; -import org.springframework.lang.Nullable; /** * Straightforward implementation of {@link org.springframework.cache.Cache.ValueWrapper}, @@ -30,8 +31,7 @@ */ public class SimpleValueWrapper implements ValueWrapper { - @Nullable - private final Object value; + private final @Nullable Object value; /** @@ -47,8 +47,7 @@ public SimpleValueWrapper(@Nullable Object value) { * Simply returns the value as given at construction time. */ @Override - @Nullable - public Object get() { + public @Nullable Object get() { return this.value; } diff --git a/spring-context/src/main/java/org/springframework/cache/support/package-info.java b/spring-context/src/main/java/org/springframework/cache/support/package-info.java index 8e82da01276e..8b3a4173c75b 100644 --- a/spring-context/src/main/java/org/springframework/cache/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/cache/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for the org.springframework.cache package. * Provides abstract classes for cache managers and caches. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.cache.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ApplicationContext.java index ed5270db78df..1c22b0d5fc11 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationContext.java @@ -16,12 +16,13 @@ package org.springframework.context; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.HierarchicalBeanFactory; import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.lang.Nullable; /** * Central interface to provide configuration for an application. @@ -62,8 +63,7 @@ public interface ApplicationContext extends EnvironmentCapable, ListableBeanFact * Return the unique id of this application context. * @return the unique id of the context, or {@code null} if none */ - @Nullable - String getId(); + @Nullable String getId(); /** * Return a name for the deployed application that this context belongs to. @@ -88,8 +88,7 @@ public interface ApplicationContext extends EnvironmentCapable, ListableBeanFact * and this is the root of the context hierarchy. * @return the parent context, or {@code null} if there is no parent */ - @Nullable - ApplicationContext getParent(); + @Nullable ApplicationContext getParent(); /** * Expose AutowireCapableBeanFactory functionality for this context. diff --git a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java index 30cdd4e4d46a..714c7a290234 100644 --- a/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.java +++ b/spring-context/src/main/java/org/springframework/context/ApplicationEventPublisher.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. @@ -52,7 +52,7 @@ public interface ApplicationEventPublisher { * instance itself. *

For the convenient inclusion of the current transaction context * in a reactive hand-off, consider using - * {@link org.springframework.transaction.reactive.TransactionalEventPublisher#publishEvent(Function)}. + * {@link org.springframework.transaction.reactive.TransactionalEventPublisher#publishEvent(java.util.function.Function)}. * For thread-bound transactions, this is not necessary since the * state will be implicitly available through thread-local storage. * @param event the event to publish diff --git a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java index 2861196a5426..d33f146834ab 100644 --- a/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/ConfigurableApplicationContext.java @@ -19,6 +19,8 @@ import java.io.Closeable; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -26,7 +28,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.ProtocolResolver; import org.springframework.core.metrics.ApplicationStartup; -import org.springframework.lang.Nullable; /** * SPI interface to be implemented by most if not all application contexts. diff --git a/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java b/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java index 2949a99c7924..8e30cae76354 100644 --- a/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/HierarchicalMessageSource.java @@ -16,7 +16,7 @@ package org.springframework.context; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Sub-interface of MessageSource to be implemented by objects that @@ -39,7 +39,6 @@ public interface HierarchicalMessageSource extends MessageSource { /** * Return the parent of this MessageSource, or {@code null} if none. */ - @Nullable - MessageSource getParentMessageSource(); + @Nullable MessageSource getParentMessageSource(); } diff --git a/spring-context/src/main/java/org/springframework/context/MessageSource.java b/spring-context/src/main/java/org/springframework/context/MessageSource.java index fd7ecbe23baf..629ae459f248 100644 --- a/spring-context/src/main/java/org/springframework/context/MessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/MessageSource.java @@ -18,7 +18,7 @@ import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface for resolving messages, with support for the parameterization @@ -54,8 +54,7 @@ public interface MessageSource { * @see #getMessage(MessageSourceResolvable, Locale) * @see java.text.MessageFormat */ - @Nullable - String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale); + @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale); /** * Try to resolve the message. Treat as an error if the message can't be found. @@ -71,7 +70,7 @@ public interface MessageSource { * @see #getMessage(MessageSourceResolvable, Locale) * @see java.text.MessageFormat */ - String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException; + String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException; /** * Try to resolve the message using all the attributes contained within the diff --git a/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java index 6908b85eddf7..11486fd6dc2a 100644 --- a/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java +++ b/spring-context/src/main/java/org/springframework/context/MessageSourceResolvable.java @@ -16,7 +16,7 @@ package org.springframework.context; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface for objects that are suitable for message resolution in a @@ -37,8 +37,7 @@ public interface MessageSourceResolvable { * they should get tried. The last code will therefore be the default one. * @return a String array of codes which are associated with this message */ - @Nullable - String[] getCodes(); + String @Nullable [] getCodes(); /** * Return the array of arguments to be used to resolve this message. @@ -47,8 +46,7 @@ public interface MessageSourceResolvable { * placeholders within the message text * @see java.text.MessageFormat */ - @Nullable - default Object[] getArguments() { + default Object @Nullable [] getArguments() { return null; } @@ -61,8 +59,7 @@ default Object[] getArguments() { * for this particular message. * @return the default message, or {@code null} if no default */ - @Nullable - default String getDefaultMessage() { + default @Nullable String getDefaultMessage() { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java index c737fc876404..dd91d5aff1b2 100644 --- a/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java +++ b/spring-context/src/main/java/org/springframework/context/PayloadApplicationEvent.java @@ -18,9 +18,10 @@ import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableTypeProvider; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java index bc2def951f3e..69ccfe579ae3 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AdviceModeImportSelector.java @@ -18,10 +18,11 @@ import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.GenericTypeResolver; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -92,7 +93,6 @@ public final String[] selectImports(AnnotationMetadata importingClassMetadata) { * @return array containing classes to import (empty array if none; * {@code null} if the given {@code AdviceMode} is unknown) */ - @Nullable - protected abstract String[] selectImports(AdviceMode adviceMode); + protected abstract String @Nullable [] selectImports(AdviceMode adviceMode); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java index 6b156b4ebb66..109eec9fd2fa 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotatedBeanDefinitionReader.java @@ -19,6 +19,8 @@ import java.lang.annotation.Annotation; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionCustomizer; @@ -30,7 +32,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -247,8 +248,8 @@ public void registerBean(Class beanClass, @Nullable String name, @Nullabl * @since 5.0 */ private void doRegisterBean(Class beanClass, @Nullable String name, - @Nullable Class[] qualifiers, @Nullable Supplier supplier, - @Nullable BeanDefinitionCustomizer[] customizers) { + Class @Nullable [] qualifiers, @Nullable Supplier supplier, + BeanDefinitionCustomizer @Nullable [] customizers) { AnnotatedGenericBeanDefinition abd = new AnnotatedGenericBeanDefinition(beanClass); if (this.conditionEvaluator.shouldSkip(abd.getMetadata())) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java index 7828fb033700..7502db9182e7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationBeanNameGenerator.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; @@ -40,7 +41,6 @@ import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -54,11 +54,8 @@ * {@link org.springframework.stereotype.Repository @Repository}) are * themselves annotated with {@code @Component}. * - *

Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and - * JSR-330's {@link jakarta.inject.Named} annotations (as well as their pre-Jakarta - * {@code javax.annotation.ManagedBean} and {@code javax.inject.Named} equivalents), - * if available. Note that Spring component annotations always override such - * standard annotations. + *

Also supports JSR-330's {@link jakarta.inject.Named} annotation if available. + * Note that Spring component annotations always override such standard annotations. * *

If the annotation's value doesn't indicate a bean name, an appropriate * name will be built based on the short name of the class (with the first @@ -125,8 +122,7 @@ public String generateBeanName(BeanDefinition definition, BeanDefinitionRegistry * @param annotatedDef the annotation-aware bean definition * @return the bean name, or {@code null} if none is found */ - @Nullable - protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { + protected @Nullable String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotatedDef) { AnnotationMetadata metadata = annotatedDef.getMetadata(); String beanName = getExplicitBeanName(metadata); @@ -151,25 +147,16 @@ protected String determineBeanNameFromAnnotation(AnnotatedBeanDefinition annotat key -> getMetaAnnotationTypes(mergedAnnotation)); if (isStereotypeWithNameValue(annotationType, metaAnnotationTypes, attributes)) { Object value = attributes.get(MergedAnnotation.VALUE); - if (value instanceof String currentName && !currentName.isBlank()) { + if (value instanceof String currentName && !currentName.isBlank() && + !hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) { if (conventionBasedStereotypeCheckCache.add(annotationType) && metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) && logger.isWarnEnabled()) { - if (hasExplicitlyAliasedValueAttribute(mergedAnnotation.getType())) { - logger.warn(""" - Although the 'value' attribute in @%s declares @AliasFor for an attribute \ - other than @Component's 'value' attribute, the value is still used as the \ - @Component name based on convention. As of Spring Framework 7.0, such a \ - 'value' attribute will no longer be used as the @Component name.""" - .formatted(annotationType)); - } - else { - logger.warn(""" - Support for convention-based @Component names is deprecated and will \ - be removed in a future version of the framework. Please annotate the \ - 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ - to declare an explicit alias for @Component's 'value' attribute.""" - .formatted(annotationType)); - } + logger.warn(""" + Support for convention-based @Component names is deprecated and will \ + be removed in a future version of the framework. Please annotate the \ + 'value' attribute in @%s with @AliasFor(annotation=Component.class) \ + to declare an explicit alias for @Component's 'value' attribute.""" + .formatted(annotationType)); } if (beanName != null && !currentName.equals(beanName)) { throw new IllegalStateException("Stereotype annotations suggest inconsistent " + @@ -201,8 +188,7 @@ private Set getMetaAnnotationTypes(MergedAnnotation mergedAn * @since 6.1 * @see org.springframework.stereotype.Component#value() */ - @Nullable - private String getExplicitBeanName(AnnotationMetadata metadata) { + private @Nullable String getExplicitBeanName(AnnotationMetadata metadata) { List names = metadata.getAnnotations().stream(COMPONENT_ANNOTATION_CLASSNAME) .map(annotation -> annotation.getString(MergedAnnotation.VALUE)) .filter(StringUtils::hasText) @@ -229,13 +215,10 @@ private String getExplicitBeanName(AnnotationMetadata metadata) { * @return whether the annotation qualifies as a stereotype with component name */ protected boolean isStereotypeWithNameValue(String annotationType, - Set metaAnnotationTypes, Map attributes) { + Set metaAnnotationTypes, Map attributes) { boolean isStereotype = metaAnnotationTypes.contains(COMPONENT_ANNOTATION_CLASSNAME) || - annotationType.equals("jakarta.annotation.ManagedBean") || - annotationType.equals("javax.annotation.ManagedBean") || - annotationType.equals("jakarta.inject.Named") || - annotationType.equals("javax.inject.Named"); + annotationType.equals("jakarta.inject.Named"); return (isStereotype && attributes.containsKey(MergedAnnotation.VALUE)); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java index 207722a4c562..814d9d0aeaf7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigApplicationContext.java @@ -19,13 +19,14 @@ import java.util.Arrays; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinitionCustomizer; import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.support.GenericApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java index 878d545deff2..4296cdecbb02 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigBeanDefinitionParser.java @@ -18,6 +18,7 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.parsing.CompositeComponentDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * Parser for the <context:annotation-config/> element. @@ -40,8 +40,7 @@ public class AnnotationConfigBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); // Obtain bean definitions for all relevant BeanPostProcessors. diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.java index 4cac35bb999a..3210fa18bfc2 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigRegistry.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. @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import org.springframework.beans.factory.BeanRegistrar; + /** * Common interface for annotation config application contexts, * defining {@link #register} and {@link #scan} methods. @@ -26,17 +28,33 @@ public interface AnnotationConfigRegistry { /** - * Register one or more component classes to be processed. + * Invoke the given registrars for registering their beans with this + * application context. + *

This can be used to register custom beans without inferring + * annotation-based characteristics for primary/fallback/lazy-init, + * rather specifying those programmatically if needed. + * @param registrars one or more {@link BeanRegistrar} instances + * @since 7.0 + * @see #register(Class[]) + */ + void register(BeanRegistrar... registrars); + + /** + * Register one or more component classes to be processed, inferring + * annotation-based characteristics for primary/fallback/lazy-init + * just like for scanned component classes. *

Calls to {@code register} are idempotent; adding the same * component class more than once has no additional effect. * @param componentClasses one or more component classes, * for example, {@link Configuration @Configuration} classes + * @see #scan(String...) */ void register(Class... componentClasses); /** * Perform a scan within the specified base packages. * @param basePackages the packages to scan for component classes + * @see #register(Class[]) */ void scan(String... basePackages); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index e427aaf5b85c..ba1214b0e6a1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -20,6 +20,8 @@ import java.util.Set; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor; import org.springframework.beans.factory.config.BeanDefinition; @@ -35,7 +37,6 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -117,9 +118,6 @@ public abstract class AnnotationConfigUtils { private static final boolean jakartaAnnotationsPresent = ClassUtils.isPresent("jakarta.annotation.PostConstruct", classLoader); - private static final boolean jsr250Present = - ClassUtils.isPresent("javax.annotation.PostConstruct", classLoader); - private static final boolean jpaPresent = ClassUtils.isPresent("jakarta.persistence.EntityManagerFactory", classLoader) && ClassUtils.isPresent(PERSISTENCE_ANNOTATION_PROCESSOR_CLASS_NAME, classLoader); @@ -169,8 +167,7 @@ public static Set registerAnnotationConfigProcessors( } // Check for Jakarta Annotations support, and if present add the CommonAnnotationBeanPostProcessor. - if ((jakartaAnnotationsPresent || jsr250Present) && - !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) { + if (jakartaAnnotationsPresent && !registry.containsBeanDefinition(COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(CommonAnnotationBeanPostProcessor.class); def.setSource(source); beanDefs.add(registerPostProcessor(registry, def, COMMON_ANNOTATION_PROCESSOR_BEAN_NAME)); @@ -214,8 +211,7 @@ private static BeanDefinitionHolder registerPostProcessor( return new BeanDefinitionHolder(definition, beanName); } - @Nullable - private static DefaultListableBeanFactory unwrapDefaultListableBeanFactory(BeanDefinitionRegistry registry) { + private static @Nullable DefaultListableBeanFactory unwrapDefaultListableBeanFactory(BeanDefinitionRegistry registry) { if (registry instanceof DefaultListableBeanFactory dlbf) { return dlbf; } @@ -275,13 +271,11 @@ static BeanDefinitionHolder applyScopedProxyMode( return ScopedProxyCreator.createScopedProxy(definition, registry, proxyTargetClass); } - @Nullable - static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annotationType) { + static @Nullable AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, Class annotationType) { return attributesFor(metadata, annotationType.getName()); } - @Nullable - static AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annotationTypeName) { + static @Nullable AnnotationAttributes attributesFor(AnnotatedTypeMetadata metadata, String annotationTypeName) { return AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(annotationTypeName)); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java index cd4d18724422..2a39fbe8fd92 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/BeanMethod.java @@ -18,11 +18,12 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; import org.springframework.core.type.MethodMetadata; -import org.springframework.lang.Nullable; /** * Represents a {@link Configuration @Configuration} class method annotated with @@ -44,7 +45,7 @@ final class BeanMethod extends ConfigurationMethod { @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Reflection public void validate(ProblemReporter problemReporter) { if (getMetadata().getAnnotationAttributes(Autowired.class.getName()) != null) { // declared as @Autowired: semantic mismatch since @Bean method arguments are autowired @@ -62,7 +63,7 @@ public void validate(ProblemReporter problemReporter) { return; } - Map attributes = + Map attributes = getConfigurationClass().getMetadata().getAnnotationAttributes(Configuration.class.getName()); if (attributes != null && (Boolean) attributes.get("proxyBeanMethods") && !getMetadata().isOverridable()) { // instance @Bean methods within @Configuration classes must be overridable to accommodate CGLIB diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java index a68de8cacb99..5292105c710b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathBeanDefinitionScanner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,6 +19,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -31,7 +33,6 @@ import org.springframework.core.env.EnvironmentCapable; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -48,8 +49,7 @@ * {@link org.springframework.stereotype.Service @Service}, or * {@link org.springframework.stereotype.Controller @Controller} stereotype. * - *

Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and - * JSR-330's {@link jakarta.inject.Named} annotations, if available. + *

Also supports JSR-330's {@link jakarta.inject.Named} annotations, if available. * * @author Mark Fisher * @author Juergen Hoeller @@ -67,8 +67,7 @@ public class ClassPathBeanDefinitionScanner extends ClassPathScanningCandidateCo private BeanDefinitionDefaults beanDefinitionDefaults = new BeanDefinitionDefaults(); - @Nullable - private String[] autowireCandidatePatterns; + private String @Nullable [] autowireCandidatePatterns; private BeanNameGenerator beanNameGenerator = AnnotationBeanNameGenerator.INSTANCE; @@ -200,7 +199,7 @@ public BeanDefinitionDefaults getBeanDefinitionDefaults() { * Set the name-matching patterns for determining autowire candidates. * @param autowireCandidatePatterns the patterns to match against */ - public void setAutowireCandidatePatterns(@Nullable String... autowireCandidatePatterns) { + public void setAutowireCandidatePatterns(String @Nullable ... autowireCandidatePatterns) { this.autowireCandidatePatterns = autowireCandidatePatterns; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java index cde1b3d8dbc2..12c92adbaef0 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProvider.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; @@ -54,7 +55,6 @@ import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.stereotype.Controller; import org.springframework.stereotype.Indexed; @@ -116,20 +116,15 @@ public class ClassPathScanningCandidateComponentProvider implements EnvironmentC private final List excludeFilters = new ArrayList<>(); - @Nullable - private Environment environment; + private @Nullable Environment environment; - @Nullable - private ConditionEvaluator conditionEvaluator; + private @Nullable ConditionEvaluator conditionEvaluator; - @Nullable - private ResourcePatternResolver resourcePatternResolver; + private @Nullable ResourcePatternResolver resourcePatternResolver; - @Nullable - private MetadataReaderFactory metadataReaderFactory; + private @Nullable MetadataReaderFactory metadataReaderFactory; - @Nullable - private CandidateComponentsIndex componentsIndex; + private @Nullable CandidateComponentsIndex componentsIndex; /** @@ -216,31 +211,12 @@ public void resetFilters(boolean useDefaultFilters) { * {@link Component @Component} meta-annotation including the * {@link Repository @Repository}, {@link Service @Service}, and * {@link Controller @Controller} stereotype annotations. - *

Also supports Jakarta EE's {@link jakarta.annotation.ManagedBean} and - * JSR-330's {@link jakarta.inject.Named} annotations (as well as their - * pre-Jakarta {@code javax.annotation.ManagedBean} and {@code javax.inject.Named} - * equivalents), if available. + *

Also supports JSR-330's {@link jakarta.inject.Named} annotation if available. */ @SuppressWarnings("unchecked") protected void registerDefaultFilters() { this.includeFilters.add(new AnnotationTypeFilter(Component.class)); ClassLoader cl = ClassPathScanningCandidateComponentProvider.class.getClassLoader(); - try { - this.includeFilters.add(new AnnotationTypeFilter( - ((Class) ClassUtils.forName("jakarta.annotation.ManagedBean", cl)), false)); - logger.trace("JSR-250 'jakarta.annotation.ManagedBean' found and supported for component scanning"); - } - catch (ClassNotFoundException ex) { - // JSR-250 1.1 API (as included in Jakarta EE) not available - simply skip. - } - try { - this.includeFilters.add(new AnnotationTypeFilter( - ((Class) ClassUtils.forName("javax.annotation.ManagedBean", cl)), false)); - logger.trace("JSR-250 'javax.annotation.ManagedBean' found and supported for component scanning"); - } - catch (ClassNotFoundException ex) { - // JSR-250 1.1 API not available - simply skip. - } try { this.includeFilters.add(new AnnotationTypeFilter( ((Class) ClassUtils.forName("jakarta.inject.Named", cl)), false)); @@ -249,14 +225,6 @@ protected void registerDefaultFilters() { catch (ClassNotFoundException ex) { // JSR-330 API (as included in Jakarta EE) not available - simply skip. } - try { - this.includeFilters.add(new AnnotationTypeFilter( - ((Class) ClassUtils.forName("javax.inject.Named", cl)), false)); - logger.trace("JSR-330 'javax.inject.Named' annotation found and supported for component scanning"); - } - catch (ClassNotFoundException ex) { - // JSR-330 API not available - simply skip. - } } /** @@ -282,8 +250,7 @@ public final Environment getEnvironment() { /** * Return the {@link BeanDefinitionRegistry} used by this scanner, if any. */ - @Nullable - protected BeanDefinitionRegistry getRegistry() { + protected @Nullable BeanDefinitionRegistry getRegistry() { return null; } @@ -395,8 +362,7 @@ private boolean indexSupportsIncludeFilter(TypeFilter filter) { * @since 5.0 * @see #indexSupportsIncludeFilter(TypeFilter) */ - @Nullable - private String extractStereotype(TypeFilter filter) { + private @Nullable String extractStereotype(TypeFilter filter) { if (filter instanceof AnnotationTypeFilter annotationTypeFilter) { return annotationTypeFilter.getAnnotationType().getName(); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java index 2d684f9875b3..8b30c0431fa4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/CommonAnnotationBeanPostProcessor.java @@ -34,6 +34,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aot.generate.AccessControl; @@ -68,7 +70,6 @@ import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.jndi.support.SimpleJndiBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -96,11 +97,6 @@ * and default names as well. The target beans can be simple POJOs, with no special * requirements other than the type having to match. * - *

Additionally, the original {@code javax.annotation} variants of the annotations - * dating back to the JSR-250 specification (Java EE 5-8, also included in JDK 6-8) - * are still supported as well. Note that this is primarily for a smooth upgrade path, - * not for adoption in new applications. - * *

This post-processor also supports the EJB {@link jakarta.ejb.EJB} annotation, * analogous to {@link jakarta.annotation.Resource}, with the capability to * specify both a local bean name and a global JNDI name for fallback retrieval. @@ -151,14 +147,9 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean private static final Set> resourceAnnotationTypes = CollectionUtils.newLinkedHashSet(3); - @Nullable - private static final Class jakartaResourceType; - - @Nullable - private static final Class javaxResourceType; + private static final @Nullable Class jakartaResourceType; - @Nullable - private static final Class ejbAnnotationType; + private static final @Nullable Class ejbAnnotationType; static { jakartaResourceType = loadAnnotationType("jakarta.annotation.Resource"); @@ -166,11 +157,6 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean resourceAnnotationTypes.add(jakartaResourceType); } - javaxResourceType = loadAnnotationType("javax.annotation.Resource"); - if (javaxResourceType != null) { - resourceAnnotationTypes.add(javaxResourceType); - } - ejbAnnotationType = loadAnnotationType("jakarta.ejb.EJB"); if (ejbAnnotationType != null) { resourceAnnotationTypes.add(ejbAnnotationType); @@ -184,17 +170,13 @@ public class CommonAnnotationBeanPostProcessor extends InitDestroyAnnotationBean private boolean alwaysUseJndiLookup = false; - @Nullable - private transient BeanFactory jndiFactory; + private transient @Nullable BeanFactory jndiFactory; - @Nullable - private transient BeanFactory resourceFactory; + private transient @Nullable BeanFactory resourceFactory; - @Nullable - private transient BeanFactory beanFactory; + private transient @Nullable BeanFactory beanFactory; - @Nullable - private transient StringValueResolver embeddedValueResolver; + private transient @Nullable StringValueResolver embeddedValueResolver; private final transient Map injectionMetadataCache = new ConcurrentHashMap<>(256); @@ -212,10 +194,6 @@ public CommonAnnotationBeanPostProcessor() { addInitAnnotationType(loadAnnotationType("jakarta.annotation.PostConstruct")); addDestroyAnnotationType(loadAnnotationType("jakarta.annotation.PreDestroy")); - // Tolerate legacy JSR-250 annotations in javax.annotation package - addInitAnnotationType(loadAnnotationType("javax.annotation.PostConstruct")); - addDestroyAnnotationType(loadAnnotationType("javax.annotation.PreDestroy")); - // java.naming module present on JDK 9+? if (jndiPresent) { this.jndiFactory = new SimpleJndiBeanFactory(); @@ -315,8 +293,7 @@ public void postProcessMergedBeanDefinition(RootBeanDefinition beanDefinition, C } @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { BeanRegistrationAotContribution parentAotContribution = super.processAheadOfTime(registeredBean); Class beanClass = registeredBean.getBeanClass(); String beanName = registeredBean.getBeanName(); @@ -333,8 +310,7 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe return parentAotContribution; } - @Nullable - private AutowireCandidateResolver getAutowireCandidateResolver(RegisteredBean registeredBean) { + private @Nullable AutowireCandidateResolver getAutowireCandidateResolver(RegisteredBean registeredBean) { if (registeredBean.getBeanFactory() instanceof DefaultListableBeanFactory lbf) { return lbf.getAutowireCandidateResolver(); } @@ -352,8 +328,7 @@ public void resetBeanDefinition(String beanName) { } @Override - @Nullable - public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + public @Nullable Object postProcessBeforeInstantiation(Class beanClass, String beanName) { return null; } @@ -444,14 +419,6 @@ else if (jakartaResourceType != null && field.isAnnotationPresent(jakartaResourc currElements.add(new ResourceElement(field, field, null)); } } - else if (javaxResourceType != null && field.isAnnotationPresent(javaxResourceType)) { - if (Modifier.isStatic(field.getModifiers())) { - throw new IllegalStateException("@Resource annotation is not supported on static fields"); - } - if (!this.ignoredResourceTypes.contains(field.getType().getName())) { - currElements.add(new LegacyResourceElement(field, field, null)); - } - } }); ReflectionUtils.doWithLocalMethods(targetClass, method -> { @@ -486,21 +453,6 @@ else if (jakartaResourceType != null && bridgedMethod.isAnnotationPresent(jakart } } } - else if (javaxResourceType != null && bridgedMethod.isAnnotationPresent(javaxResourceType)) { - if (method.equals(ClassUtils.getMostSpecificMethod(method, clazz))) { - if (Modifier.isStatic(method.getModifiers())) { - throw new IllegalStateException("@Resource annotation is not supported on static methods"); - } - Class[] paramTypes = method.getParameterTypes(); - if (paramTypes.length != 1) { - throw new IllegalStateException("@Resource annotation requires a single-arg method: " + method); - } - if (!this.ignoredResourceTypes.contains(paramTypes[0].getName())) { - PropertyDescriptor pd = BeanUtils.findPropertyForMethod(bridgedMethod, clazz); - currElements.add(new LegacyResourceElement(method, bridgedMethod, pd)); - } - } - } }); elements.addAll(0, currElements); @@ -625,8 +577,7 @@ protected Object autowireResource(BeanFactory factory, LookupElement element, @N @SuppressWarnings("unchecked") - @Nullable - private static Class loadAnnotationType(String name) { + private static @Nullable Class loadAnnotationType(String name) { try { return (Class) ClassUtils.forName(name, CommonAnnotationBeanPostProcessor.class.getClassLoader()); @@ -649,8 +600,7 @@ protected abstract static class LookupElement extends InjectionMetadata.Injected protected Class lookupType = Object.class; - @Nullable - protected String mappedName; + protected @Nullable String mappedName; public LookupElement(Member member, @Nullable PropertyDescriptor pd) { super(member, pd); @@ -746,57 +696,6 @@ boolean isLazyLookup() { } - /** - * Class representing injection information about an annotated field - * or setter method, supporting the @Resource annotation. - */ - private class LegacyResourceElement extends LookupElement { - - private final boolean lazyLookup; - - public LegacyResourceElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { - super(member, pd); - javax.annotation.Resource resource = ae.getAnnotation(javax.annotation.Resource.class); - String resourceName = resource.name(); - Class resourceType = resource.type(); - this.isDefaultName = !StringUtils.hasLength(resourceName); - if (this.isDefaultName) { - resourceName = this.member.getName(); - if (this.member instanceof Method && resourceName.startsWith("set") && resourceName.length() > 3) { - resourceName = StringUtils.uncapitalizeAsProperty(resourceName.substring(3)); - } - } - else if (embeddedValueResolver != null) { - resourceName = embeddedValueResolver.resolveStringValue(resourceName); - } - if (Object.class != resourceType) { - checkResourceType(resourceType); - } - else { - // No resource type specified... check field/method. - resourceType = getResourceType(); - } - this.name = (resourceName != null ? resourceName : ""); - this.lookupType = resourceType; - String lookupValue = resource.lookup(); - this.mappedName = (StringUtils.hasLength(lookupValue) ? lookupValue : resource.mappedName()); - Lazy lazy = ae.getAnnotation(Lazy.class); - this.lazyLookup = (lazy != null && lazy.value()); - } - - @Override - protected Object getResourceToInject(Object target, @Nullable String requestingBeanName) { - return (this.lazyLookup ? buildLazyResourceProxy(this, requestingBeanName) : - getResource(this, requestingBeanName)); - } - - @Override - boolean isLazyLookup() { - return this.lazyLookup; - } - } - - /** * Class representing injection information about an annotated field * or setter method, supporting the @EJB annotation. @@ -866,8 +765,7 @@ private static class AotContribution implements BeanRegistrationAotContribution private final Collection lookupElements; - @Nullable - private final AutowireCandidateResolver candidateResolver; + private final @Nullable AutowireCandidateResolver candidateResolver; AotContribution(Class target, Collection lookupElements, @Nullable AutowireCandidateResolver candidateResolver) { @@ -960,7 +858,7 @@ private CodeBlock generateMethodStatementForMethod(ClassName targetClassName, return CodeBlock.of("$L.resolveAndSet($L, $L)", resolver, REGISTERED_BEAN_PARAMETER, INSTANCE_PARAMETER); } - hints.reflection().registerMethod(method, ExecutableMode.INTROSPECT); + hints.reflection().registerType(method.getDeclaringClass()); return CodeBlock.of("$L.$L($L.resolve($L))", INSTANCE_PARAMETER, method.getName(), resolver, REGISTERED_BEAN_PARAMETER); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java index d38a670c9582..3a1a9ed2cc00 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ComponentScanBeanDefinitionParser.java @@ -20,6 +20,7 @@ import java.util.Set; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.w3c.dom.Node; import org.w3c.dom.NodeList; @@ -39,7 +40,6 @@ import org.springframework.core.type.filter.AssignableTypeFilter; import org.springframework.core.type.filter.RegexPatternTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -79,8 +79,7 @@ public class ComponentScanBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { String basePackage = element.getAttribute(BASE_PACKAGE_ATTRIBUTE); basePackage = parserContext.getReaderContext().getEnvironment().resolvePlaceholders(basePackage); String[] basePackages = StringUtils.tokenizeToStringArray(basePackage, diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java index e28dfda7a309..cdf0cee02eb7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionContext.java @@ -16,11 +16,12 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; /** * Context information for use by {@link Condition} implementations. @@ -44,8 +45,7 @@ public interface ConditionContext { * definition should the condition match, or {@code null} if the bean factory is * not available (or not downcastable to {@code ConfigurableListableBeanFactory}). */ - @Nullable - ConfigurableListableBeanFactory getBeanFactory(); + @Nullable ConfigurableListableBeanFactory getBeanFactory(); /** * Return the {@link Environment} for which the current application is running. @@ -62,7 +62,6 @@ public interface ConditionContext { * (only {@code null} if even the system ClassLoader isn't accessible). * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) */ - @Nullable - ClassLoader getClassLoader(); + @Nullable ClassLoader getClassLoader(); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java b/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java index bccce9326f68..25554c1015af 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConditionEvaluator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; @@ -33,7 +35,6 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; @@ -128,7 +129,7 @@ List collectConditions(@Nullable AnnotatedTypeMetadata metadata) { @SuppressWarnings("unchecked") private List getConditionClasses(AnnotatedTypeMetadata metadata) { - MultiValueMap attributes = metadata.getAllAnnotationAttributes(Conditional.class.getName(), true); + MultiValueMap attributes = metadata.getAllAnnotationAttributes(Conditional.class.getName(), true); Object values = (attributes != null ? attributes.get("value") : null); return (List) (values != null ? values : Collections.emptyList()); } @@ -144,18 +145,15 @@ private Condition getCondition(String conditionClassName, @Nullable ClassLoader */ private static class ConditionContextImpl implements ConditionContext { - @Nullable - private final BeanDefinitionRegistry registry; + private final @Nullable BeanDefinitionRegistry registry; - @Nullable - private final ConfigurableListableBeanFactory beanFactory; + private final @Nullable ConfigurableListableBeanFactory beanFactory; private final Environment environment; private final ResourceLoader resourceLoader; - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; public ConditionContextImpl(@Nullable BeanDefinitionRegistry registry, @Nullable Environment environment, @Nullable ResourceLoader resourceLoader) { @@ -167,8 +165,7 @@ public ConditionContextImpl(@Nullable BeanDefinitionRegistry registry, this.classLoader = deduceClassLoader(resourceLoader, this.beanFactory); } - @Nullable - private static ConfigurableListableBeanFactory deduceBeanFactory(@Nullable BeanDefinitionRegistry source) { + private static @Nullable ConfigurableListableBeanFactory deduceBeanFactory(@Nullable BeanDefinitionRegistry source) { if (source instanceof ConfigurableListableBeanFactory configurableListableBeanFactory) { return configurableListableBeanFactory; } @@ -192,8 +189,7 @@ private static ResourceLoader deduceResourceLoader(@Nullable BeanDefinitionRegis return new DefaultResourceLoader(); } - @Nullable - private static ClassLoader deduceClassLoader(@Nullable ResourceLoader resourceLoader, + private static @Nullable ClassLoader deduceClassLoader(@Nullable ResourceLoader resourceLoader, @Nullable ConfigurableListableBeanFactory beanFactory) { if (resourceLoader != null) { @@ -215,8 +211,7 @@ public BeanDefinitionRegistry getRegistry() { } @Override - @Nullable - public ConfigurableListableBeanFactory getBeanFactory() { + public @Nullable ConfigurableListableBeanFactory getBeanFactory() { return this.beanFactory; } @@ -231,8 +226,7 @@ public ResourceLoader getResourceLoader() { } @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return this.classLoader; } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java index 96314a730230..b279917f6365 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Configuration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -471,7 +471,10 @@ * Switch this flag to {@code false} in order to allow for method overloading * according to those semantics, accepting the risk for accidental overlaps. * @since 6.0 + * @deprecated as of 7.0, always relying on {@code @Bean} unique methods, + * just possibly with {@code Optional}/{@code ObjectProvider} arguments */ + @Deprecated(since = "7.0") boolean enforceUniqueMethods() default true; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java index 53daf3105ba3..22cc4968659f 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClass.java @@ -22,6 +22,9 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.parsing.Location; import org.springframework.beans.factory.parsing.Problem; import org.springframework.beans.factory.parsing.ProblemReporter; @@ -31,7 +34,6 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -53,8 +55,7 @@ final class ConfigurationClass { private final Resource resource; - @Nullable - private String beanName; + private @Nullable String beanName; private boolean scanned = false; @@ -65,6 +66,8 @@ final class ConfigurationClass { private final Map> importedResources = new LinkedHashMap<>(); + private final Map beanRegistrars = new LinkedHashMap<>(); + private final Map importBeanDefinitionRegistrars = new LinkedHashMap<>(); @@ -154,8 +157,7 @@ void setBeanName(@Nullable String beanName) { this.beanName = beanName; } - @Nullable - String getBeanName() { + @Nullable String getBeanName() { return this.beanName; } @@ -220,6 +222,14 @@ Map> getImportedResources() { return this.importedResources; } + void addBeanRegistrar(String sourceClassName, BeanRegistrar beanRegistrar) { + this.beanRegistrars.put(sourceClassName, beanRegistrar); + } + + public Map getBeanRegistrars() { + return this.beanRegistrars; + } + void addImportBeanDefinitionRegistrar(ImportBeanDefinitionRegistrar registrar, AnnotationMetadata importingClassMetadata) { this.importBeanDefinitionRegistrars.put(registrar, importingClassMetadata); } @@ -228,9 +238,9 @@ Map getImportBeanDefinitionRe return this.importBeanDefinitionRegistrars; } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Reflection void validate(ProblemReporter problemReporter) { - Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); + Map attributes = this.metadata.getAnnotationAttributes(Configuration.class.getName()); // A configuration class may not be final (CGLIB limitation) unless it does not have to proxy bean methods if (attributes != null && (Boolean) attributes.get("proxyBeanMethods") && hasNonStaticBeanMethods() && 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 8385b9ef34e8..5e2e827d6e52 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassBeanDefinitionReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.Arrays; @@ -26,8 +27,12 @@ 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; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; @@ -40,6 +45,7 @@ import org.springframework.beans.factory.support.BeanDefinitionReader; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.BeanRegistryAdapter; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.annotation.ConfigurationCondition.ConfigurationPhase; @@ -51,7 +57,6 @@ import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.StandardAnnotationMetadata; import org.springframework.core.type.StandardMethodMetadata; -import org.springframework.lang.NonNull; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -146,7 +151,8 @@ private void loadBeanDefinitionsForConfigurationClass( } loadBeanDefinitionsFromImportedResources(configClass.getImportedResources()); - loadBeanDefinitionsFromRegistrars(configClass.getImportBeanDefinitionRegistrars()); + loadBeanDefinitionsFromImportBeanDefinitionRegistrars(configClass.getImportBeanDefinitionRegistrars()); + loadBeanDefinitionsFromBeanRegistrars(configClass.getBeanRegistrars()); } /** @@ -286,12 +292,13 @@ private void loadBeanDefinitionsForBeanMethod(BeanMethod beanMethod) { } 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); } + @SuppressWarnings("NullAway") // Reflection protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String beanName) { if (!this.registry.containsBeanDefinition(beanName)) { return false; @@ -302,21 +309,23 @@ protected boolean isOverriddenByExistingDefinition(BeanMethod beanMethod, String // If the bean method is an overloaded case on the same configuration class, // preserve the existing bean definition and mark it as overloaded. if (existingBeanDef instanceof ConfigurationClassBeanDefinition ccbd) { - if (ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { - if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) { - ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); - } - else if (!this.registry.isBeanDefinitionOverridable(beanName)) { - throw new BeanDefinitionOverrideException(beanName, - new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), - existingBeanDef, - "@Bean method override with same bean name but different method name: " + existingBeanDef); - } + if (!ccbd.getMetadata().getClassName().equals(configClass.getMetadata().getClassName())) { + return false; + } + if (ccbd.getFactoryMethodMetadata().getMethodName().equals(beanMethod.getMetadata().getMethodName())) { + ccbd.setNonUniqueFactoryMethodName(ccbd.getFactoryMethodMetadata().getMethodName()); return true; } - else { - return false; + Map attributes = + configClass.getMetadata().getAnnotationAttributes(Configuration.class.getName()); + if ((attributes != null && (Boolean) attributes.get("enforceUniqueMethods")) || + !this.registry.isBeanDefinitionOverridable(beanName)) { + throw new BeanDefinitionOverrideException(beanName, + new ConfigurationClassBeanDefinition(configClass, beanMethod.getMetadata(), beanName), + existingBeanDef, + "@Bean method override with same bean name but different method name: " + existingBeanDef); } + return true; } // A bean definition resulting from a component scan can be silently overridden @@ -344,9 +353,8 @@ else if (!this.registry.isBeanDefinitionOverridable(beanName)) { "@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; } @@ -372,9 +380,11 @@ private void loadBeanDefinitionsFromImportedResources( 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); @@ -383,20 +393,26 @@ private void loadBeanDefinitionsFromImportedResources( } 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); }); } - private void loadBeanDefinitionsFromRegistrars(Map registrars) { + private void loadBeanDefinitionsFromImportBeanDefinitionRegistrars(Map registrars) { registrars.forEach((registrar, metadata) -> registrar.registerBeanDefinitions(metadata, this.registry, this.importBeanNameGenerator)); } + private void loadBeanDefinitionsFromBeanRegistrars(Map registrars) { + Assert.isInstanceOf(ListableBeanFactory.class, this.registry, + "Cannot support bean registrars since " + this.registry.getClass().getName() + + " does not implement BeanDefinitionRegistry"); + registrars.values().forEach(registrar -> registrar.register(new BeanRegistryAdapter(this.registry, + (ListableBeanFactory) this.registry, this.environment, registrar.getClass()), this.environment)); + } + /** * {@link RootBeanDefinition} marker subclass used to signify that a bean definition @@ -445,7 +461,6 @@ public AnnotationMetadata getMetadata() { } @Override - @NonNull public MethodMetadata getFactoryMethodMetadata() { return this.factoryMethodMetadata; } 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 9b50adddef17..beb6fcd7dec7 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; @@ -24,8 +25,10 @@ import org.apache.commons.logging.Log; 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; @@ -50,7 +53,6 @@ import org.springframework.cglib.transform.ClassEmitterTransformer; import org.springframework.cglib.transform.TransformingClassGenerator; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisException; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.Assert; @@ -115,6 +117,12 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) boolean classLoaderMismatch = (classLoader != null && classLoader != configClass.getClassLoader()); if (classLoaderMismatch && classLoader instanceof SmartClassLoader smartClassLoader) { classLoader = smartClassLoader.getOriginalClassLoader(); + classLoaderMismatch = (classLoader != configClass.getClassLoader()); + } + // Use original ClassLoader if config class relies on package visibility + if (classLoaderMismatch && reliesOnPackageVisibility(configClass)) { + classLoader = configClass.getClassLoader(); + classLoaderMismatch = false; } Enhancer enhancer = newEnhancer(configClass, classLoader); Class enhancedClass = createClass(enhancer, classLoaderMismatch); @@ -131,6 +139,32 @@ public Class enhance(Class configClass, @Nullable ClassLoader classLoader) } } + /** + * Checks whether the given config class relies on package visibility, either for + * the class and any of its constructors or for any of its {@code @Bean} methods. + */ + private boolean reliesOnPackageVisibility(Class configSuperClass) { + int mod = configSuperClass.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + for (Constructor ctor : configSuperClass.getDeclaredConstructors()) { + mod = ctor.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } + for (Method method : ReflectionUtils.getDeclaredMethods(configSuperClass)) { + if (BeanAnnotationHelper.isBeanAnnotated(method)) { + mod = method.getModifiers(); + if (!Modifier.isPublic(mod) && !Modifier.isProtected(mod)) { + return true; + } + } + } + return false; + } + /** * Creates a new CGLIB {@link Enhancer} instance. */ @@ -138,26 +172,22 @@ private Enhancer newEnhancer(Class configSuperClass, @Nullable ClassLoader cl Enhancer enhancer = new Enhancer(); if (classLoader != null) { enhancer.setClassLoader(classLoader); + if (classLoader instanceof SmartClassLoader smartClassLoader && + smartClassLoader.isClassReloadable(configSuperClass)) { + enhancer.setUseCache(false); + } } enhancer.setSuperclass(configSuperClass); enhancer.setInterfaces(new Class[] {EnhancedConfiguration.class}); enhancer.setUseFactory(false); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(!isClassReloadable(configSuperClass, classLoader)); + enhancer.setAttemptLoad(enhancer.getUseCache() && AotDetector.useGeneratedArtifacts()); enhancer.setStrategy(new BeanFactoryAwareGeneratorStrategy(classLoader)); enhancer.setCallbackFilter(CALLBACK_FILTER); enhancer.setCallbackTypes(CALLBACK_FILTER.getCallbackTypes()); return enhancer; } - /** - * Checks whether the given configuration class is reloadable. - */ - private boolean isClassReloadable(Class configSuperClass, @Nullable ClassLoader classLoader) { - return (classLoader instanceof SmartClassLoader smartClassLoader && - smartClassLoader.isClassReloadable(configSuperClass)); - } - /** * Uses enhancer to generate a subclass of superclass, * ensuring that callbacks are registered for the new subclass. @@ -276,8 +306,7 @@ public void end_class() { private static class BeanFactoryAwareMethodInterceptor implements MethodInterceptor, ConditionalCallback { @Override - @Nullable - public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { + public @Nullable Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable { Field field = ReflectionUtils.findField(obj.getClass(), BEAN_FACTORY_FIELD); Assert.state(field != null, "Unable to find generated BeanFactory field"); field.set(obj, args[0]); @@ -319,8 +348,7 @@ private static class BeanMethodInterceptor implements MethodInterceptor, Conditi * super implementation of the proxied method i.e., the actual {@code @Bean} method */ @Override - @Nullable - public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, + public @Nullable Object intercept(Object enhancedConfigInstance, Method beanMethod, Object[] beanMethodArgs, MethodProxy cglibMethodProxy) throws Throwable { ConfigurableBeanFactory beanFactory = getBeanFactory(enhancedConfigInstance); @@ -341,9 +369,9 @@ public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object // 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 } @@ -373,8 +401,7 @@ public Object intercept(Object enhancedConfigInstance, Method beanMethod, Object return resolveBeanReference(beanMethod, beanMethodArgs, beanFactory, beanName); } - @Nullable - private Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, + private @Nullable Object resolveBeanReference(Method beanMethod, Object[] beanMethodArgs, ConfigurableBeanFactory beanFactory, String beanName) { // The user (i.e. not the factory) is requesting this bean through a call to @@ -548,7 +575,7 @@ private Object createCglibProxyForFactoryBean(Object factoryBean, Enhancer enhancer = new Enhancer(); enhancer.setSuperclass(factoryBean.getClass()); enhancer.setNamingPolicy(SpringNamingPolicy.INSTANCE); - enhancer.setAttemptLoad(true); + enhancer.setAttemptLoad(AotDetector.useGeneratedArtifacts()); enhancer.setCallbackType(MethodInterceptor.class); // Ideally create enhanced FactoryBean proxy without constructor side effects, diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassParser.java index ba3f926fd74c..1f88fbea9fb4 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 @@ -37,8 +37,11 @@ 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.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; @@ -69,7 +72,6 @@ import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AssignableTypeFilter; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -96,6 +98,7 @@ * @author Phillip Webb * @author Sam Brannen * @author Stephane Nicoll + * @author Daeho Kwon * @since 3.0 * @see ConfigurationClassBeanDefinitionReader */ @@ -105,8 +108,8 @@ class ConfigurationClassParser { (className.startsWith("java.lang.annotation.") || className.startsWith("org.springframework.stereotype.")); private static final Predicate REGISTER_BEAN_CONDITION_FILTER = condition -> - (condition instanceof ConfigurationCondition configurationCondition - && ConfigurationPhase.REGISTER_BEAN.equals(configurationCondition.getConfigurationPhase())); + (condition instanceof ConfigurationCondition configurationCondition && + ConfigurationPhase.REGISTER_BEAN.equals(configurationCondition.getConfigurationPhase())); private static final Comparator DEFERRED_IMPORT_COMPARATOR = (o1, o2) -> AnnotationAwareOrderComparator.INSTANCE.compare(o1.getImportSelector(), o2.getImportSelector()); @@ -122,8 +125,7 @@ class ConfigurationClassParser { private final ResourceLoader resourceLoader; - @Nullable - private final PropertySourceRegistry propertySourceRegistry; + private final @Nullable PropertySourceRegistry propertySourceRegistry; private final BeanDefinitionRegistry registry; @@ -179,8 +181,9 @@ else if (bd instanceof AbstractBeanDefinition abstractBeanDef && abstractBeanDef } // Downgrade to lite (no enhancement) in case of no instance-level @Bean methods. - if (!configClass.hasNonStaticBeanMethods() && ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( - bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { + if (!configClass.getMetadata().isAbstract() && !configClass.hasNonStaticBeanMethods() && + ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals( + bd.getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE))) { bd.setAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE, ConfigurationClassUtils.CONFIGURATION_CLASS_LITE); } @@ -297,8 +300,7 @@ else if (configClass.isScanned()) { * @param sourceClass a source class * @return the superclass, or {@code null} if none found or previously processed */ - @Nullable - protected final SourceClass doProcessConfigurationClass( + protected final @Nullable SourceClass doProcessConfigurationClass( ConfigurationClass configClass, SourceClass sourceClass, Predicate filter) throws IOException { @@ -548,15 +550,22 @@ private Set getImports(SourceClass sourceClass) throws IOException *

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())) { @@ -599,6 +608,15 @@ private void processImports(ConfigurationClass configClass, SourceClass currentS processImports(configClass, currentSourceClass, importSourceClasses, filter, false); } } + else if (candidate.isAssignable(BeanRegistrar.class)) { + Class candidateClass = candidate.loadClass(); + BeanRegistrar registrar = (BeanRegistrar) BeanUtils.instantiateClass(candidateClass); + AnnotationMetadata metadata = currentSourceClass.getMetadata(); + if (registrar instanceof ImportAware importAware) { + importAware.setImportMetadata(metadata); + } + configClass.addBeanRegistrar(metadata.getClassName(), registrar); + } else if (candidate.isAssignable(ImportBeanDefinitionRegistrar.class)) { // Candidate class is an ImportBeanDefinitionRegistrar -> // delegate to it to register additional bean definitions @@ -720,8 +738,7 @@ private List collectRegisterBeanConditions(ConfigurationClass configu return allConditions.stream().filter(REGISTER_BEAN_CONDITION_FILTER).toList(); } - @Nullable - private ConfigurationClass getEnclosingConfigurationClass(ConfigurationClass configurationClass) { + private @Nullable ConfigurationClass getEnclosingConfigurationClass(ConfigurationClass configurationClass) { String enclosingClassName = configurationClass.getMetadata().getEnclosingClassName(); if (enclosingClassName != null) { return configurationClass.getImportedBy().stream() @@ -742,8 +759,7 @@ void registerImport(AnnotationMetadata importingClass, String importedClass) { } @Override - @Nullable - public AnnotationMetadata getImportingClassFor(String importedClass) { + public @Nullable AnnotationMetadata getImportingClassFor(String importedClass) { return CollectionUtils.lastElement(this.imports.get(importedClass)); } @@ -782,8 +798,7 @@ public String toString() { private class DeferredImportSelectorHandler { - @Nullable - private List deferredImportSelectors = new ArrayList<>(); + private @Nullable List deferredImportSelectors = new ArrayList<>(); /** * Handle the specified {@link DeferredImportSelector}. If deferred import @@ -839,12 +854,12 @@ void register(DeferredImportSelectorHolder deferredImport) { deferredImport.getConfigurationClass()); } - @SuppressWarnings("NullAway") void processGroupImports() { for (DeferredImportSelectorGrouping grouping : this.groupings.values()) { Predicate filter = grouping.getCandidateFilter(); grouping.getImports().forEach(entry -> { ConfigurationClass configurationClass = this.configurationClasses.get(entry.getMetadata()); + Assert.state(configurationClass != null, "ConfigurationClass must not be null"); try { processImports(configurationClass, asSourceClass(configurationClass, filter), Collections.singleton(asSourceClass(entry.getImportClassName(), filter)), @@ -1095,7 +1110,7 @@ public Set getAnnotations() { } public Collection getAnnotationAttributes(String annType, String attribute) throws IOException { - Map annotationAttributes = this.metadata.getAnnotationAttributes(annType, true); + Map annotationAttributes = this.metadata.getAnnotationAttributes(annType, true); if (annotationAttributes == null || !annotationAttributes.containsKey(attribute)) { return Collections.emptySet(); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java index 52823f67fa30..c11e97210379 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassPostProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,9 @@ import java.io.UncheckedIOException; import java.lang.reflect.Constructor; import java.lang.reflect.Executable; +import java.lang.reflect.Method; import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -36,12 +38,16 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; @@ -49,7 +55,10 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; +import org.springframework.beans.factory.aot.AotServices; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; @@ -60,6 +69,7 @@ import org.springframework.beans.factory.aot.BeanRegistrationCodeFragmentsDecorator; import org.springframework.beans.factory.aot.InstanceSupplierCodeGenerator; import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionCustomizer; import org.springframework.beans.factory.config.BeanDefinitionHolder; import org.springframework.beans.factory.config.BeanFactoryPostProcessor; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; @@ -73,6 +83,7 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; import org.springframework.beans.factory.support.BeanNameGenerator; +import org.springframework.beans.factory.support.BeanRegistryAdapter; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RegisteredBean.InstantiationDescriptor; @@ -99,14 +110,20 @@ import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.NameAllocator; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; +import org.springframework.util.StringUtils; /** * {@link BeanFactoryPostProcessor} used for bootstrapping processing of @@ -153,13 +170,11 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private ProblemReporter problemReporter = new FailFastProblemReporter(); - @Nullable - private Environment environment; + private @Nullable Environment environment; private ResourceLoader resourceLoader = new DefaultResourceLoader(); - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(); @@ -169,8 +184,7 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private final Set factoriesPostProcessed = new HashSet<>(); - @Nullable - private ConfigurationClassBeanDefinitionReader reader; + private @Nullable ConfigurationClassBeanDefinitionReader reader; private boolean localBeanNameGeneratorSet = false; @@ -182,9 +196,11 @@ public class ConfigurationClassPostProcessor implements BeanDefinitionRegistryPo private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; - @Nullable + @SuppressWarnings("NullAway.Init") private List propertySourceDescriptors; + private Map beanRegistrars = new LinkedHashMap<>(); + @Override public int getOrder() { @@ -312,9 +328,8 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) beanFactory.addBeanPostProcessor(new ImportAwareBeanPostProcessor(beanFactory)); } - @Nullable @Override - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Object configClassAttr = registeredBean.getMergedBeanDefinition() .getAttribute(ConfigurationClassUtils.CONFIGURATION_CLASS_ATTRIBUTE); if (ConfigurationClassUtils.CONFIGURATION_CLASS_FULL.equals(configClassAttr)) { @@ -325,12 +340,11 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe } @Override - @Nullable - @SuppressWarnings("NullAway") - public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { boolean hasPropertySourceDescriptors = !CollectionUtils.isEmpty(this.propertySourceDescriptors); boolean hasImportRegistry = beanFactory.containsBean(IMPORT_REGISTRY_BEAN_NAME); - if (hasPropertySourceDescriptors || hasImportRegistry) { + boolean hasBeanRegistrars = !this.beanRegistrars.isEmpty(); + if (hasPropertySourceDescriptors || hasImportRegistry || hasBeanRegistrars) { return (generationContext, code) -> { if (hasPropertySourceDescriptors) { new PropertySourcesAotContribution(this.propertySourceDescriptors, this::resolvePropertySourceLocation) @@ -339,13 +353,15 @@ public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableL if (hasImportRegistry) { new ImportAwareAotContribution(beanFactory).applyTo(generationContext, code); } + if (hasBeanRegistrars) { + new BeanRegistrarAotContribution(this.beanRegistrars, beanFactory).applyTo(generationContext, code); + } }; } return null; } - @Nullable - private Resource resolvePropertySourceLocation(String location) { + private @Nullable Resource resolvePropertySourceLocation(String location) { try { String resolvedLocation = (this.environment != null ? this.environment.resolveRequiredPlaceholders(location) : location); @@ -428,6 +444,9 @@ else if (ConfigurationClassUtils.checkConfigurationClassCandidate(beanDef, this. this.importBeanNameGenerator, parser.getImportRegistry()); } this.reader.loadBeanDefinitions(configClasses); + for (ConfigurationClass configClass : configClasses) { + this.beanRegistrars.putAll(configClass.getBeanRegistrars()); + } alreadyParsed.addAll(configClasses); processConfig.tag("classCount", () -> String.valueOf(configClasses.size())).end(); @@ -558,8 +577,7 @@ public ImportAwareBeanPostProcessor(BeanFactory beanFactory) { } @Override - @Nullable - public PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) { + public @Nullable PropertyValues postProcessProperties(@Nullable PropertyValues pvs, Object bean, String beanName) { // Inject the BeanFactory before AutowiredAnnotationBeanPostProcessor's // postProcessProperties method attempts to autowire other configuration beans. if (bean instanceof EnhancedConfiguration enhancedConfiguration) { @@ -667,9 +685,9 @@ private static class PropertySourcesAotContribution implements BeanFactoryInitia private final List descriptors; - private final Function resourceResolver; + private final Function resourceResolver; - PropertySourcesAotContribution(List descriptors, Function resourceResolver) { + PropertySourcesAotContribution(List descriptors, Function resourceResolver) { this.descriptors = descriptors; this.resourceResolver = resourceResolver; } @@ -812,7 +830,7 @@ private InstantiationDescriptor proxyInstantiationDescriptor( Executable userExecutable = instantiationDescriptor.executable(); if (userExecutable instanceof Constructor userConstructor) { try { - runtimeHints.reflection().registerConstructor(userConstructor, ExecutableMode.INTROSPECT); + runtimeHints.reflection().registerType(userConstructor.getDeclaringClass()); Constructor constructor = this.proxyClass.getConstructor(userExecutable.getParameterTypes()); return new InstantiationDescriptor(constructor); } @@ -824,4 +842,201 @@ private InstantiationDescriptor proxyInstantiationDescriptor( } } + private static class BeanRegistrarAotContribution implements BeanFactoryInitializationAotContribution { + + private static final String CUSTOMIZER_MAP_VARIABLE = "customizers"; + + private static final String ENVIRONMENT_VARIABLE = "environment"; + + private final Map beanRegistrars; + + private final ConfigurableListableBeanFactory beanFactory; + + private final AotServices aotProcessors; + + public BeanRegistrarAotContribution(Map beanRegistrars, ConfigurableListableBeanFactory beanFactory) { + this.beanRegistrars = beanRegistrars; + this.beanFactory = beanFactory; + this.aotProcessors = AotServices.factoriesAndBeans(this.beanFactory).load(BeanRegistrationAotProcessor.class); + } + + @Override + public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { + GeneratedMethod generatedMethod = beanFactoryInitializationCode.getMethods().add( + "applyBeanRegistrars", builder -> this.generateApplyBeanRegistrarsMethod(builder, generationContext)); + beanFactoryInitializationCode.addInitializer(generatedMethod.toMethodReference()); + } + + private void generateApplyBeanRegistrarsMethod(MethodSpec.Builder method, GenerationContext generationContext) { + ReflectionHints reflectionHints = generationContext.getRuntimeHints().reflection(); + method.addJavadoc("Apply bean registrars."); + method.addModifiers(Modifier.PRIVATE); + method.addParameter(ListableBeanFactory.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE); + method.addParameter(Environment.class, ENVIRONMENT_VARIABLE); + method.addCode(generateCustomizerMap()); + + for (String name : this.beanFactory.getBeanDefinitionNames()) { + BeanDefinition beanDefinition = this.beanFactory.getMergedBeanDefinition(name); + if (beanDefinition.getSource() instanceof Class sourceClass && + BeanRegistrar.class.isAssignableFrom(sourceClass)) { + + for (BeanRegistrationAotProcessor aotProcessor : this.aotProcessors) { + BeanRegistrationAotContribution contribution = + aotProcessor.processAheadOfTime(RegisteredBean.of(this.beanFactory, name)); + if (contribution != null) { + contribution.applyTo(generationContext, + new UnsupportedBeanRegistrationCode(name, aotProcessor.getClass())); + } + } + if (beanDefinition instanceof RootBeanDefinition rootBeanDefinition) { + if (rootBeanDefinition.getPreferredConstructors() != null) { + for (Constructor constructor : rootBeanDefinition.getPreferredConstructors()) { + reflectionHints.registerConstructor(constructor, ExecutableMode.INVOKE); + } + } + if (!ObjectUtils.isEmpty(rootBeanDefinition.getInitMethodNames())) { + method.addCode(generateInitDestroyMethods(name, rootBeanDefinition, + rootBeanDefinition.getInitMethodNames(), "setInitMethodNames", reflectionHints)); + } + if (!ObjectUtils.isEmpty(rootBeanDefinition.getDestroyMethodNames())) { + method.addCode(generateInitDestroyMethods(name, rootBeanDefinition, + rootBeanDefinition.getDestroyMethodNames(), "setDestroyMethodNames", reflectionHints)); + } + checkUnsupportedFeatures(rootBeanDefinition); + } + } + } + method.addCode(generateRegisterCode()); + } + + private void checkUnsupportedFeatures(AbstractBeanDefinition beanDefinition) { + if (!ObjectUtils.isEmpty(beanDefinition.getFactoryBeanName())) { + throw new UnsupportedOperationException("AOT post processing of the factory bean name is not supported yet with BeanRegistrar"); + } + if (beanDefinition.hasConstructorArgumentValues()) { + throw new UnsupportedOperationException("AOT post processing of argument values is not supported yet with BeanRegistrar"); + } + if (!beanDefinition.getQualifiers().isEmpty()) { + throw new UnsupportedOperationException("AOT post processing of qualifiers is not supported yet with BeanRegistrar"); + } + for (String attributeName : beanDefinition.attributeNames()) { + if (!attributeName.equals(AbstractBeanDefinition.ORDER_ATTRIBUTE) && + !attributeName.equals("aotProcessingIgnoreRegistration")) { + throw new UnsupportedOperationException("AOT post processing of attribute " + attributeName + + " is not supported yet with BeanRegistrar"); + } + } + } + + private CodeBlock generateCustomizerMap() { + Builder code = CodeBlock.builder(); + code.addStatement("$T<$T, $T> $L = new $T<>()", MultiValueMap.class, String.class, BeanDefinitionCustomizer.class, + CUSTOMIZER_MAP_VARIABLE, LinkedMultiValueMap.class); + return code.build(); + } + + private CodeBlock generateRegisterCode() { + Builder code = CodeBlock.builder(); + Builder metadataReaderFactoryCode = null; + NameAllocator nameAllocator = new NameAllocator(); + for (Map.Entry beanRegistrarEntry : this.beanRegistrars.entrySet()) { + BeanRegistrar beanRegistrar = beanRegistrarEntry.getValue(); + String beanRegistrarName = nameAllocator.newName(StringUtils.uncapitalize(beanRegistrar.getClass().getSimpleName())); + code.addStatement("$T $L = new $T()", beanRegistrar.getClass(), beanRegistrarName, beanRegistrar.getClass()); + if (beanRegistrar instanceof ImportAware) { + if (metadataReaderFactoryCode == null) { + metadataReaderFactoryCode = CodeBlock.builder(); + metadataReaderFactoryCode.addStatement("$T metadataReaderFactory = new $T()", + MetadataReaderFactory.class, CachingMetadataReaderFactory.class); + } + code.beginControlFlow("try") + .addStatement("$L.setImportMetadata(metadataReaderFactory.getMetadataReader($S).getAnnotationMetadata())", + beanRegistrarName, beanRegistrarEntry.getKey()) + .nextControlFlow("catch ($T ex)", IOException.class) + .addStatement("throw new $T(\"Failed to read metadata for '$L'\", ex)", + IllegalStateException.class, beanRegistrarEntry.getKey()) + .endControlFlow(); + } + code.addStatement("$L.register(new $T(($T)$L, $L, $L, $T.class, $L), $L)", beanRegistrarName, + BeanRegistryAdapter.class, BeanDefinitionRegistry.class, BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, + BeanFactoryInitializationCode.BEAN_FACTORY_VARIABLE, ENVIRONMENT_VARIABLE, beanRegistrar.getClass(), + CUSTOMIZER_MAP_VARIABLE, ENVIRONMENT_VARIABLE); + } + return (metadataReaderFactoryCode == null ? code.build() : metadataReaderFactoryCode.add(code.build()).build()); + } + + private CodeBlock generateInitDestroyMethods(String beanName, AbstractBeanDefinition beanDefinition, + String[] methodNames, String method, ReflectionHints reflectionHints) { + + Builder code = CodeBlock.builder(); + // For Publisher-based destroy methods + reflectionHints.registerType(TypeReference.of("org.reactivestreams.Publisher")); + Class beanType = ClassUtils.getUserClass(beanDefinition.getResolvableType().toClass()); + Arrays.stream(methodNames).forEach(methodName -> addInitDestroyHint(beanType, methodName, reflectionHints)); + CodeBlock arguments = Arrays.stream(methodNames) + .map(name -> CodeBlock.of("$S", name)) + .collect(CodeBlock.joining(", ")); + + code.addStatement("$L.add($S, $L -> (($T)$L).$L($L))", CUSTOMIZER_MAP_VARIABLE, beanName, "bd", + AbstractBeanDefinition.class, "bd", method, arguments); + return code.build(); + } + + // Inspired from BeanDefinitionPropertiesCodeGenerator#addInitDestroyHint + private static void addInitDestroyHint(Class beanUserClass, String methodName, ReflectionHints reflectionHints) { + Class methodDeclaringClass = beanUserClass; + + // Parse fully-qualified method name if necessary. + int indexOfDot = methodName.lastIndexOf('.'); + if (indexOfDot > 0) { + String className = methodName.substring(0, indexOfDot); + methodName = methodName.substring(indexOfDot + 1); + if (!beanUserClass.getName().equals(className)) { + try { + methodDeclaringClass = ClassUtils.forName(className, beanUserClass.getClassLoader()); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to load Class [" + className + + "] from ClassLoader [" + beanUserClass.getClassLoader() + "]", ex); + } + } + } + + Method method = ReflectionUtils.findMethod(methodDeclaringClass, methodName); + if (method != null) { + reflectionHints.registerMethod(method, ExecutableMode.INVOKE); + Method publiclyAccessibleMethod = ClassUtils.getPubliclyAccessibleMethodIfPossible(method, beanUserClass); + if (!publiclyAccessibleMethod.equals(method)) { + reflectionHints.registerMethod(publiclyAccessibleMethod, ExecutableMode.INVOKE); + } + } + } + + + static class UnsupportedBeanRegistrationCode implements BeanRegistrationCode { + + private final String message; + + public UnsupportedBeanRegistrationCode(String beanName, Class aotProcessorClass) { + this.message = "Code generation attempted for bean " + beanName + " by the AOT Processor " + + aotProcessorClass + " is not supported with BeanRegistrar yet"; + } + + @Override + public ClassName getClassName() { + throw new UnsupportedOperationException(this.message); + } + + @Override + public GeneratedMethods getMethods() { + throw new UnsupportedOperationException(this.message); + } + + @Override + public void addInstancePostProcessor(MethodReference methodReference) { + throw new UnsupportedOperationException(this.message); + } + } + } + } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java index 4d557ae9ea49..da17f4140730 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ConfigurationClassUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; @@ -38,7 +39,6 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; /** @@ -143,7 +143,7 @@ else if (beanDef instanceof AbstractBeanDefinition abstractBd && abstractBd.hasB } } - Map config = metadata.getAnnotationAttributes(Configuration.class.getName()); + Map config = metadata.getAnnotationAttributes(Configuration.class.getName()); if (config != null && !Boolean.FALSE.equals(config.get("proxyBeanMethods"))) { beanDef.setAttribute(CONFIGURATION_CLASS_ATTRIBUTE, CONFIGURATION_CLASS_FULL); } @@ -207,9 +207,8 @@ static boolean hasBeanMethods(AnnotationMetadata metadata) { * or {@code Ordered.LOWEST_PRECEDENCE} if none declared * @since 5.0 */ - @Nullable - public static Integer getOrder(AnnotationMetadata metadata) { - Map orderAttributes = metadata.getAnnotationAttributes(Order.class.getName()); + public static @Nullable Integer getOrder(AnnotationMetadata metadata) { + Map orderAttributes = metadata.getAnnotationAttributes(Order.class.getName()); return (orderAttributes != null ? ((Integer) orderAttributes.get(AnnotationUtils.VALUE)) : null); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java index bc012d05807c..944f1d4c8454 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ContextAnnotationAutowireCandidateResolver.java @@ -26,6 +26,8 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -34,7 +36,6 @@ import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; /** * Complete implementation of the @@ -48,14 +49,12 @@ public class ContextAnnotationAutowireCandidateResolver extends QualifierAnnotationAutowireCandidateResolver { @Override - @Nullable - public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { + public @Nullable Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) { return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null); } @Override - @Nullable - public Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { + public @Nullable Class getLazyResolutionProxyClass(DependencyDescriptor descriptor, @Nullable String beanName) { return (isLazy(descriptor) ? (Class) buildLazyResolutionProxy(descriptor, beanName, true) : null); } @@ -110,11 +109,9 @@ private static class LazyDependencyTargetSource implements TargetSource, Seriali private final DependencyDescriptor descriptor; - @Nullable - private final String beanName; + private final @Nullable String beanName; - @Nullable - private transient volatile Object cachedTarget; + private transient volatile @Nullable Object cachedTarget; public LazyDependencyTargetSource(DefaultListableBeanFactory beanFactory, DependencyDescriptor descriptor, @Nullable String beanName) { @@ -130,7 +127,6 @@ public Class getTargetClass() { } @Override - @SuppressWarnings("NullAway") public Object getTarget() { Object cachedTarget = this.cachedTarget; if (cachedTarget != null) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java index dedb068b6497..c55a548ce49b 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/DeferredImportSelector.java @@ -16,8 +16,9 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; /** * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans @@ -43,8 +44,7 @@ public interface DeferredImportSelector extends ImportSelector { * @return the import group class, or {@code null} if none * @since 5.0 */ - @Nullable - default Class getImportGroup() { + default @Nullable Class getImportGroup() { return null; } 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 9c905d0d0b18..7d26dc6f8bd5 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 @@ -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. @@ -22,14 +22,18 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.beans.factory.BeanRegistrar; + /** * Indicates one or more component classes to import — typically * {@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} @@ -37,7 +41,17 @@ * 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. @@ -57,7 +71,8 @@ /** * {@link Configuration @Configuration}, {@link ImportSelector}, - * {@link ImportBeanDefinitionRegistrar}, or regular component classes to import. + * {@link ImportBeanDefinitionRegistrar}, {@link BeanRegistrar}, or regular + * component classes to import. */ Class[] value(); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessor.java index 24f422e1e8ce..c026153a9ba4 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportAwareAotBeanPostProcessor.java @@ -19,13 +19,14 @@ import java.io.IOException; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.core.Ordered; import org.springframework.core.PriorityOrdered; import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -75,8 +76,7 @@ private void setAnnotationMetadata(ImportAware instance) { } } - @Nullable - private String getImportingClassFor(ImportAware instance) { + private @Nullable String getImportingClassFor(ImportAware instance) { String target = ClassUtils.getUserClass(instance).getName(); return this.importsMapping.get(target); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java index 779bbc3058c8..335f7fe80ae6 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportRegistry.java @@ -16,8 +16,9 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; /** * Registry of imported class {@link AnnotationMetadata}. @@ -27,8 +28,7 @@ */ interface ImportRegistry { - @Nullable - AnnotationMetadata getImportingClassFor(String importedClass); + @Nullable AnnotationMetadata getImportingClassFor(String importedClass); void removeImportingClass(String importingClass); 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 da1cb97b8e40..dbb1c6047dff 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 @@ * 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 @@ /** * {@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/annotation/ImportSelector.java b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java index 7ae3423ec596..804fb337beae 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ImportSelector.java @@ -18,8 +18,9 @@ import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; /** * Interface to be implemented by types that determine which @{@link Configuration} @@ -77,8 +78,7 @@ public interface ImportSelector { * of transitively imported configuration classes, or {@code null} if none * @since 5.2.4 */ - @Nullable - default Predicate getExclusionFilter() { + default @Nullable Predicate getExclusionFilter() { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java index 06ab3313eccc..45100980c2f7 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/Jsr330ScopeMetadataResolver.java @@ -20,9 +20,10 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.lang.Nullable; /** * Simple {@link ScopeMetadataResolver} implementation that follows JSR-330 scoping rules: @@ -77,8 +78,7 @@ public final void registerScope(String annotationType, String scopeName) { * @param annotationType the JSR-330 annotation type * @return the Spring scope name */ - @Nullable - protected String resolveScopeName(String annotationType) { + protected @Nullable String resolveScopeName(String annotationType) { return this.scopeMap.get(annotationType); } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java index 86b1ede8f709..064bd1ac28a0 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/LoadTimeWeavingConfiguration.java @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.config.BeanDefinition; @@ -26,7 +28,6 @@ import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; import org.springframework.instrument.classloading.LoadTimeWeaver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -45,14 +46,11 @@ @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class LoadTimeWeavingConfiguration implements ImportAware, BeanClassLoaderAware { - @Nullable - private AnnotationAttributes enableLTW; + private @Nullable AnnotationAttributes enableLTW; - @Nullable - private LoadTimeWeavingConfigurer ltwConfigurer; + private @Nullable LoadTimeWeavingConfigurer ltwConfigurer; - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; @Override diff --git a/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java b/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java index 25715f82cad7..85ffe5fbb994 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/MBeanExportConfiguration.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. @@ -20,6 +20,8 @@ import javax.management.MBeanServer; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.BeanDefinition; @@ -29,7 +31,6 @@ import org.springframework.core.type.AnnotationMetadata; import org.springframework.jmx.export.annotation.AnnotationMBeanExporter; import org.springframework.jmx.support.RegistrationPolicy; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -50,19 +51,16 @@ public class MBeanExportConfiguration implements ImportAware, EnvironmentAware, private static final String MBEAN_EXPORTER_BEAN_NAME = "mbeanExporter"; - @Nullable - private AnnotationAttributes enableMBeanExport; + private @Nullable AnnotationAttributes enableMBeanExport; - @Nullable - private Environment environment; + private @Nullable Environment environment; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; @Override public void setImportMetadata(AnnotationMetadata importMetadata) { - Map map = importMetadata.getAnnotationAttributes(EnableMBeanExport.class.getName()); + Map map = importMetadata.getAnnotationAttributes(EnableMBeanExport.class.getName()); this.enableMBeanExport = AnnotationAttributes.fromMap(map); if (this.enableMBeanExport == null) { throw new IllegalArgumentException( diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java index adbfbecb457f..bc682d476fde 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ParserStrategyUtils.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 @@ import java.lang.reflect.Constructor; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.Aware; @@ -30,7 +32,6 @@ import org.springframework.context.ResourceLoaderAware; import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -75,7 +76,7 @@ private static Object createInstance(Class clazz, Environment environment, if (constructors.length == 1 && constructors[0].getParameterCount() > 0) { try { Constructor constructor = constructors[0]; - Object[] args = resolveArgs(constructor.getParameterTypes(), + @Nullable Object[] args = resolveArgs(constructor.getParameterTypes(), environment, resourceLoader, registry, classLoader); return BeanUtils.instantiateClass(constructor, args); } @@ -86,11 +87,11 @@ private static Object createInstance(Class clazz, Environment environment, return BeanUtils.instantiateClass(clazz); } - private static Object[] resolveArgs(Class[] parameterTypes, + private static @Nullable Object[] resolveArgs(Class[] parameterTypes, Environment environment, ResourceLoader resourceLoader, BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { - Object[] parameters = new Object[parameterTypes.length]; + @Nullable Object[] parameters = new Object[parameterTypes.length]; for (int i = 0; i < parameterTypes.length; i++) { parameters[i] = resolveParameter(parameterTypes[i], environment, resourceLoader, registry, classLoader); @@ -98,8 +99,7 @@ private static Object[] resolveArgs(Class[] parameterTypes, return parameters; } - @Nullable - private static Object resolveParameter(Class parameterType, + private static @Nullable Object resolveParameter(Class parameterType, Environment environment, ResourceLoader resourceLoader, BeanDefinitionRegistry registry, @Nullable ClassLoader classLoader) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java index a720639e7cb6..e5c0a36463f1 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ProfileCondition.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,8 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.type.AnnotatedTypeMetadata; import org.springframework.util.MultiValueMap; @@ -31,9 +33,9 @@ class ProfileCondition implements Condition { @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Reflection public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); + MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName()); if (attrs != null) { for (Object value : attrs.get("value")) { if (context.getEnvironment().matchesProfiles((String[]) value)) { diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java b/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java index d3a78ea91b52..eaf2a86d7508 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ResourceElementResolver.java @@ -23,6 +23,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -30,7 +32,6 @@ import org.springframework.beans.factory.config.DependencyDescriptor; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -115,9 +116,8 @@ private static String defaultResourceNameForMethod(String methodName) { * @param registeredBean the registered bean * @return the resolved field or method parameter value */ - @Nullable @SuppressWarnings("unchecked") - public T resolve(RegisteredBean registeredBean) { + public @Nullable T resolve(RegisteredBean registeredBean) { Assert.notNull(registeredBean, "'registeredBean' must not be null"); return (T) (isLazyLookup(registeredBean) ? buildLazyResourceProxy(registeredBean) : resolveValue(registeredBean)); diff --git a/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java b/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java index 26e603bbf259..b9fbdab9c503 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/ScannedGenericBeanDefinition.java @@ -16,13 +16,14 @@ package org.springframework.context.annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -70,8 +71,7 @@ public final AnnotationMetadata getMetadata() { } @Override - @Nullable - public MethodMetadata getFactoryMethodMetadata() { + public @Nullable MethodMetadata getFactoryMethodMetadata() { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/package-info.java b/spring-context/src/main/java/org/springframework/context/annotation/package-info.java index d40f541f125a..cc6e50066b6d 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/package-info.java @@ -3,9 +3,7 @@ * annotations, component-scanning, and Java-based metadata for creating * Spring-managed objects. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 58c63fc117e5..2d60fd612dbf 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,13 +17,15 @@ package org.springframework.context.aot; import java.io.IOException; +import java.io.UncheckedIOException; import java.nio.file.Path; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.FileSystemGeneratedFiles; import org.springframework.aot.generate.GeneratedFiles.Kind; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.nativex.FileNativeConfigurationWriter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.FileSystemUtils; @@ -102,7 +104,7 @@ private void deleteExistingOutput(Path... paths) { 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); } } } @@ -197,20 +199,15 @@ public String getArtifactId() { */ public static final class Builder { - @Nullable - private Path sourceOutput; + private @Nullable Path sourceOutput; - @Nullable - private Path resourceOutput; + private @Nullable Path resourceOutput; - @Nullable - private Path classOutput; + private @Nullable Path classOutput; - @Nullable - private String groupId; + private @Nullable String groupId; - @Nullable - private String artifactId; + private @Nullable String artifactId; private Builder() { // internal constructor diff --git a/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java b/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java index 47744cb6f723..8d7f7c6e61ab 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java +++ b/spring-context/src/main/java/org/springframework/context/aot/AotApplicationContextInitializer.java @@ -18,13 +18,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; 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 0331b145c10e..eb5c736fafb9 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/ApplicationContextInitializationCodeGenerator.java b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java index 5305508b9da9..85cb17618724 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ApplicationContextInitializationCodeGenerator.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,11 +23,14 @@ import javax.lang.model.element.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedMethods; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -44,7 +47,6 @@ import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.javapoet.TypeName; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; /** * Internal code generator to create the {@link ApplicationContextInitializer}. @@ -139,23 +141,22 @@ public void addInitializer(MethodReference methodReference) { this.initializers.add(methodReference); } - private static class InitializerMethodArgumentCodeGenerator implements Function { + private static class InitializerMethodArgumentCodeGenerator implements Function { @Override - @Nullable - public CodeBlock apply(TypeName typeName) { + public @Nullable CodeBlock apply(TypeName typeName) { return (typeName instanceof ClassName className ? apply(className) : null); } - @Nullable - private CodeBlock apply(ClassName className) { + private @Nullable CodeBlock apply(ClassName className) { String name = className.canonicalName(); - if (name.equals(DefaultListableBeanFactory.class.getName()) - || name.equals(ConfigurableListableBeanFactory.class.getName())) { + if (name.equals(DefaultListableBeanFactory.class.getName()) || + name.equals(ListableBeanFactory.class.getName()) || + name.equals(ConfigurableListableBeanFactory.class.getName())) { return CodeBlock.of(BEAN_FACTORY_VARIABLE); } - else if (name.equals(ConfigurableEnvironment.class.getName()) - || name.equals(Environment.class.getName())) { + else if (name.equals(ConfigurableEnvironment.class.getName()) || + name.equals(Environment.class.getName())) { return CodeBlock.of("$L.getEnvironment()", APPLICATION_CONTEXT_VARIABLE); } else if (name.equals(ResourceLoader.class.getName())) { diff --git a/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java b/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java index d2d5ec73bef6..f549ae46566e 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java +++ b/spring-context/src/main/java/org/springframework/context/aot/BeanFactoryInitializationAotContributions.java @@ -20,6 +20,8 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.aot.AotException; import org.springframework.beans.factory.aot.AotProcessingException; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.support.DefaultListableBeanFactory; -import org.springframework.lang.Nullable; /** * A collection of {@link BeanFactoryInitializationAotContribution AOT @@ -74,8 +75,7 @@ private List getContributions( return Collections.unmodifiableList(contributions); } - @Nullable - private BeanFactoryInitializationAotContribution processAheadOfTime(BeanFactoryInitializationAotProcessor processor, + private @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(BeanFactoryInitializationAotProcessor processor, DefaultListableBeanFactory beanFactory) { try { 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 16fbee9bb3a1..746e23cfd6f9 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 @@ protected Class getApplicationClass() { @Override protected ClassName doProcess() { deleteExistingOutput(); - GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass()); - return performAotProcessing(applicationContext); + try (GenericApplicationContext applicationContext = prepareApplicationContext(getApplicationClass())) { + return performAotProcessing(applicationContext); + } } /** diff --git a/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java index e8804416eeeb..9820b07bcd77 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessor.java @@ -16,15 +16,15 @@ package org.springframework.context.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; import org.springframework.beans.factory.aot.BeanRegistrationCode; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.KotlinDetector; -import org.springframework.lang.Nullable; /** * AOT {@code BeanRegistrationAotProcessor} that adds additional hints @@ -36,8 +36,7 @@ class KotlinReflectionBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); if (KotlinDetector.isKotlinType(beanClass)) { return new AotContribution(beanClass); @@ -61,7 +60,7 @@ public void applyTo(GenerationContext generationContext, BeanRegistrationCode be private void registerHints(Class type, RuntimeHints runtimeHints) { if (KotlinDetector.isKotlinType(type)) { - runtimeHints.reflection().registerType(type, MemberCategory.INTROSPECT_DECLARED_METHODS); + runtimeHints.reflection().registerType(type); } Class superClass = type.getSuperclass(); if (superClass != null) { diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java index 4b6a7b79182a..80b622b45fb1 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilder.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,6 +22,8 @@ import java.util.Set; import java.util.stream.StreamSupport; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.annotation.Reflective; @@ -33,7 +35,6 @@ import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -95,11 +96,11 @@ public ReflectiveProcessorAotContributionBuilder scan(@Nullable ClassLoader clas return withClasses(scanner.scan(packageNames)); } - @Nullable - public BeanFactoryInitializationAotContribution build() { + public @Nullable BeanFactoryInitializationAotContribution build() { return (!this.classes.isEmpty() ? new AotContribution(this.classes) : null); } + private static class AotContribution implements BeanFactoryInitializationAotContribution { private final Class[] classes; @@ -113,13 +114,12 @@ public void applyTo(GenerationContext generationContext, BeanFactoryInitializati RuntimeHints runtimeHints = generationContext.getRuntimeHints(); registrar.registerRuntimeHints(runtimeHints, this.classes); } - } + private static class ReflectiveClassPathScanner extends ClassPathScanningCandidateComponentProvider { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; ReflectiveClassPathScanner(@Nullable ClassLoader classLoader) { super(false); diff --git a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java index 0ba81ec97138..193a201f5137 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessor.java @@ -21,6 +21,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.annotation.Reflective; import org.springframework.aot.hint.annotation.ReflectiveProcessor; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; @@ -29,7 +31,6 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.context.annotation.ReflectiveScan; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -43,8 +44,7 @@ class ReflectiveProcessorBeanFactoryInitializationAotProcessor implements BeanFactoryInitializationAotProcessor { @Override - @Nullable - public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + public @Nullable BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { Class[] beanClasses = Arrays.stream(beanFactory.getBeanDefinitionNames()) .map(beanName -> RegisteredBean.of(beanFactory, beanName).getBeanClass()) .toArray(Class[]::new); diff --git a/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java b/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java index 75fa1bb2c612..d79a7c8926b6 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessor.java @@ -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.generate.GenerationContext; import org.springframework.aot.hint.RuntimeHints; @@ -35,7 +36,6 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -94,8 +94,7 @@ static class RuntimeHintsRegistrarContribution implements BeanFactoryInitializat private final Iterable registrars; - @Nullable - private final ClassLoader beanClassLoader; + private final @Nullable ClassLoader beanClassLoader; RuntimeHintsRegistrarContribution(Iterable registrars, @Nullable ClassLoader beanClassLoader) { diff --git a/spring-context/src/main/java/org/springframework/context/aot/package-info.java b/spring-context/src/main/java/org/springframework/context/aot/package-info.java index abb17630288a..c9da373f170d 100644 --- a/spring-context/src/main/java/org/springframework/context/aot/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/aot/package-info.java @@ -1,9 +1,7 @@ /** * AOT support for application contexts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.aot; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java index e21b5fe2fd57..8246b6497de2 100644 --- a/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/config/LoadTimeWeaverBeanDefinitionParser.java @@ -16,6 +16,7 @@ package org.springframework.context.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.weaving.AspectJWeavingEnabler; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** 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 72504ae805c5..277ee04402ce 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/config/SpringConfiguredBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java index 2fd447a835e4..3f797dce89f2 100644 --- a/spring-context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/context/config/SpringConfiguredBeanDefinitionParser.java @@ -16,6 +16,7 @@ package org.springframework.context.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; @@ -23,7 +24,6 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; /** * {@link BeanDefinitionParser} responsible for parsing the @@ -45,8 +45,7 @@ class SpringConfiguredBeanDefinitionParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { if (!parserContext.getRegistry().containsBeanDefinition(BEAN_CONFIGURER_ASPECT_BEAN_NAME)) { RootBeanDefinition def = new RootBeanDefinition(); def.setBeanClassName(BEAN_CONFIGURER_ASPECT_CLASS_NAME); diff --git a/spring-context/src/main/java/org/springframework/context/config/package-info.java b/spring-context/src/main/java/org/springframework/context/config/package-info.java index 08b96ec341ea..65c0cc7fff34 100644 --- a/spring-context/src/main/java/org/springframework/context/config/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/config/package-info.java @@ -2,9 +2,7 @@ * Support package for advanced application context configuration, * with XML schema being the primary configuration format. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java index c552e5791593..a3f3cd83e73d 100644 --- a/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/AbstractApplicationEventMulticaster.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; @@ -37,7 +39,6 @@ import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -70,11 +71,9 @@ public abstract class AbstractApplicationEventMulticaster final Map retrieverCache = new ConcurrentHashMap<>(64); - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; - @Nullable - private ConfigurableBeanFactory beanFactory; + private @Nullable ConfigurableBeanFactory beanFactory; @Override @@ -230,7 +229,7 @@ protected Collection> getApplicationListeners( * @param retriever the ListenerRetriever, if supposed to populate one (for caching purposes) * @return the pre-filtered list of application listeners for the given event and source type */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private Collection> retrieveApplicationListeners( ResolvableType eventType, @Nullable Class sourceType, @Nullable CachedListenerRetriever retriever) { @@ -407,8 +406,7 @@ private static final class ListenerCacheKey implements Comparable sourceType; + private final @Nullable Class sourceType; public ListenerCacheKey(ResolvableType eventType, @Nullable Class sourceType) { Assert.notNull(eventType, "Event type must not be null"); @@ -457,14 +455,11 @@ public int compareTo(ListenerCacheKey other) { */ private class CachedListenerRetriever { - @Nullable - public volatile Set> applicationListeners; + public volatile @Nullable Set> applicationListeners; - @Nullable - public volatile Set applicationListenerBeans; + public volatile @Nullable Set applicationListenerBeans; - @Nullable - public Collection> getApplicationListeners() { + public @Nullable Collection> getApplicationListeners() { Set> applicationListeners = this.applicationListeners; Set applicationListenerBeans = this.applicationListenerBeans; if (applicationListeners == null || applicationListenerBeans == null) { diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java index cfbdd769a22d..2052d32b9a12 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationEventMulticaster.java @@ -18,10 +18,11 @@ import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * Interface to be implemented by objects that can manage a number of diff --git a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java index e1916d086575..97fedefef235 100644 --- a/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/ApplicationListenerMethodAdapter.java @@ -29,6 +29,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -46,7 +47,7 @@ import org.springframework.core.ResolvableType; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.Order; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -88,21 +89,17 @@ public class ApplicationListenerMethodAdapter implements GenericApplicationListe private final List declaredEventTypes; - @Nullable - private final String condition; + private final @Nullable String condition; private final boolean defaultExecution; private final int order; - @Nullable - private volatile String listenerId; + private volatile @Nullable String listenerId; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private EventExpressionEvaluator evaluator; + private @Nullable EventExpressionEvaluator evaluator; /** @@ -249,7 +246,7 @@ protected boolean isDefaultExecution() { * @param event the event to process through the listener method */ public void processEvent(ApplicationEvent event) { - Object[] args = resolveArguments(event); + @Nullable Object[] args = resolveArguments(event); if (shouldHandle(event, args)) { Object result = doInvoke(args); if (result != null) { @@ -271,7 +268,8 @@ public boolean shouldHandle(ApplicationEvent event) { return shouldHandle(event, resolveArguments(event)); } - private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) { + @Contract("_, null -> false") + private boolean shouldHandle(ApplicationEvent event, @Nullable Object @Nullable [] args) { if (args == null) { return false; } @@ -290,8 +288,7 @@ private boolean shouldHandle(ApplicationEvent event, @Nullable Object[] args) { * Can return {@code null} to indicate that no suitable arguments could be resolved * and therefore the method should not be invoked at all for the specified event. */ - @Nullable - protected Object[] resolveArguments(ApplicationEvent event) { + protected @Nullable Object @Nullable [] resolveArguments(ApplicationEvent event) { ResolvableType declaredEventType = getResolvableType(event); if (declaredEventType == null) { return null; @@ -310,7 +307,6 @@ protected Object[] resolveArguments(ApplicationEvent event) { return new Object[] {event}; } - @SuppressWarnings({"removal", "unchecked", "deprecation"}) protected void handleResult(Object result) { if (reactiveStreamsPresent && new ReactiveResultHandler().subscribeToPublisher(result)) { if (logger.isTraceEnabled()) { @@ -327,9 +323,6 @@ else if (event != null) { } }); } - else if (result instanceof org.springframework.util.concurrent.ListenableFuture listenableFuture) { - listenableFuture.addCallback(this::publishEvents, this::handleAsyncError); - } else { publishEvents(result); } @@ -366,8 +359,7 @@ protected void handleAsyncError(Throwable t) { /** * Invoke the event listener method with the given argument values. */ - @Nullable - protected Object doInvoke(@Nullable Object... args) { + protected @Nullable Object doInvoke(@Nullable Object... args) { Object bean = getTargetBean(); // Detect package-protected NullBean instance through equals(null) check if (bean.equals(null)) { @@ -423,8 +415,7 @@ protected Method getTargetMethod() { * annotation or any matching attribute on a composed annotation that * is meta-annotated with {@code @EventListener}. */ - @Nullable - protected String getCondition() { + protected @Nullable String getCondition() { return this.condition; } @@ -460,8 +451,7 @@ private void assertTargetBean(Method method, Object targetBean, @Nullable Object } } - @SuppressWarnings("NullAway") - private String getInvocationErrorMessage(Object bean, @Nullable String message, @Nullable Object[] resolvedArgs) { + private String getInvocationErrorMessage(Object bean, @Nullable String message, @Nullable Object [] resolvedArgs) { StringBuilder sb = new StringBuilder(getDetailedErrorMessage(bean, message)); sb.append("Resolved arguments: \n"); for (int i = 0; i < resolvedArgs.length; i++) { @@ -477,8 +467,7 @@ private String getInvocationErrorMessage(Object bean, @Nullable String message, return sb.toString(); } - @Nullable - private ResolvableType getResolvableType(ApplicationEvent event) { + private @Nullable ResolvableType getResolvableType(ApplicationEvent event) { ResolvableType payloadType = null; if (event instanceof PayloadApplicationEvent payloadEvent) { ResolvableType eventType = payloadEvent.getResolvableType(); diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java index acef8846be64..385288989ab8 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionEvaluator.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; import org.springframework.context.expression.AnnotatedElementKey; import org.springframework.context.expression.CachedExpressionEvaluator; @@ -51,7 +53,7 @@ class EventExpressionEvaluator extends CachedExpressionEvaluator { * to {@code true}. */ public boolean condition(String conditionExpression, ApplicationEvent event, Method targetMethod, - AnnotatedElementKey methodKey, Object[] args) { + AnnotatedElementKey methodKey, @Nullable Object[] args) { EventExpressionRootObject rootObject = new EventExpressionRootObject(event, args); EvaluationContext evaluationContext = createEvaluationContext(rootObject, targetMethod, args); @@ -60,7 +62,7 @@ public boolean condition(String conditionExpression, ApplicationEvent event, Met } private EvaluationContext createEvaluationContext(EventExpressionRootObject rootObject, - Method method, Object[] args) { + Method method, @Nullable Object[] args) { MethodBasedEvaluationContext evaluationContext = new MethodBasedEvaluationContext(rootObject, method, args, getParameterNameDiscoverer()); diff --git a/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java b/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java index fca84af09e26..c04acd097836 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventExpressionRootObject.java @@ -16,6 +16,8 @@ package org.springframework.context.event; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; /** @@ -28,5 +30,5 @@ * @param args the arguments supplied to the listener method * @see EventListener#condition() */ -record EventExpressionRootObject(ApplicationEvent event, Object[] args) { +record EventExpressionRootObject(ApplicationEvent event, @Nullable Object[] args) { } diff --git a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java index 5f330e754526..de3db6c05d5d 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventListenerMethodProcessor.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.autoproxy.AutoProxyUtils; import org.springframework.aop.scope.ScopedObject; @@ -44,7 +45,6 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.stereotype.Component; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -67,19 +67,15 @@ public class EventListenerMethodProcessor protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private ConfigurableApplicationContext applicationContext; + private @Nullable ConfigurableApplicationContext applicationContext; - @Nullable - private ConfigurableListableBeanFactory beanFactory; + private @Nullable ConfigurableListableBeanFactory beanFactory; - @Nullable - private List eventListenerFactories; + private @Nullable List eventListenerFactories; private final StandardEvaluationContext originalEvaluationContext; - @Nullable - private final EventExpressionEvaluator evaluator; + private final @Nullable EventExpressionEvaluator evaluator; private final Set> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64); diff --git a/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java b/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java index ef5c2e6e3627..324c24240b75 100644 --- a/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/context/event/EventPublicationInterceptor.java @@ -20,12 +20,12 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -50,11 +50,9 @@ public class EventPublicationInterceptor implements MethodInterceptor, ApplicationEventPublisherAware, InitializingBean { - @Nullable - private Constructor applicationEventClassConstructor; + private @Nullable Constructor applicationEventClassConstructor; - @Nullable - private ApplicationEventPublisher applicationEventPublisher; + private @Nullable ApplicationEventPublisher applicationEventPublisher; /** @@ -94,8 +92,7 @@ public void afterPropertiesSet() throws Exception { @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Object retVal = invocation.proceed(); Assert.state(this.applicationEventClassConstructor != null, "No ApplicationEvent class set"); diff --git a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java index a0ac143fa309..ec22230acbb6 100644 --- a/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java +++ b/spring-context/src/main/java/org/springframework/context/event/GenericApplicationListenerAdapter.java @@ -18,12 +18,13 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -43,8 +44,7 @@ public class GenericApplicationListenerAdapter implements GenericApplicationList private final ApplicationListener delegate; - @Nullable - private final ResolvableType declaredEventType; + private final @Nullable ResolvableType declaredEventType; /** @@ -95,8 +95,7 @@ public String getListenerId() { } - @Nullable - private static ResolvableType resolveDeclaredEventType(ApplicationListener listener) { + private static @Nullable ResolvableType resolveDeclaredEventType(ApplicationListener listener) { ResolvableType declaredEventType = resolveDeclaredEventType(listener.getClass()); if (declaredEventType == null || declaredEventType.isAssignableFrom(ApplicationEvent.class)) { Class targetClass = AopUtils.getTargetClass(listener); @@ -107,8 +106,7 @@ private static ResolvableType resolveDeclaredEventType(ApplicationListener listenerType) { + static @Nullable ResolvableType resolveDeclaredEventType(Class listenerType) { ResolvableType eventType = eventTypeCache.get(listenerType); if (eventType == null) { eventType = ResolvableType.forClass(listenerType).as(ApplicationListener.class).getGeneric(); diff --git a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java index d2fe7feb5c05..6b60b7d68bf4 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java +++ b/spring-context/src/main/java/org/springframework/context/event/SimpleApplicationEventMulticaster.java @@ -21,13 +21,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.context.PayloadApplicationEvent; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ErrorHandler; /** @@ -51,14 +51,11 @@ */ public class SimpleApplicationEventMulticaster extends AbstractApplicationEventMulticaster { - @Nullable - private Executor taskExecutor; + private @Nullable Executor taskExecutor; - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; - @Nullable - private volatile Log lazyLogger; + private volatile @Nullable Log lazyLogger; /** @@ -100,8 +97,7 @@ public void setTaskExecutor(@Nullable Executor taskExecutor) { * Return the current task executor for this multicaster. * @since 2.0 */ - @Nullable - protected Executor getTaskExecutor() { + protected @Nullable Executor getTaskExecutor() { return this.taskExecutor; } @@ -128,8 +124,7 @@ public void setErrorHandler(@Nullable ErrorHandler errorHandler) { * Return the current error handler for this multicaster. * @since 4.1 */ - @Nullable - protected ErrorHandler getErrorHandler() { + protected @Nullable ErrorHandler getErrorHandler() { return this.errorHandler; } diff --git a/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java b/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java index c56dd33b336a..24767a5f9149 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/SmartApplicationListener.java @@ -16,10 +16,11 @@ package org.springframework.context.event; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; /** * Extended variant of the standard {@link ApplicationListener} interface, diff --git a/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java b/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java index 7170931aaa0f..14ce1f25d38d 100644 --- a/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java +++ b/spring-context/src/main/java/org/springframework/context/event/SourceFilteringListener.java @@ -16,11 +16,12 @@ package org.springframework.context.event; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; import org.springframework.context.ApplicationListener; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; /** * {@link org.springframework.context.ApplicationListener} decorator that filters @@ -38,8 +39,7 @@ public class SourceFilteringListener implements GenericApplicationListener { private final Object source; - @Nullable - private GenericApplicationListener delegate; + private @Nullable GenericApplicationListener delegate; /** diff --git a/spring-context/src/main/java/org/springframework/context/event/package-info.java b/spring-context/src/main/java/org/springframework/context/event/package-info.java index 79cccd7a46ca..381af6a5dbaf 100644 --- a/spring-context/src/main/java/org/springframework/context/event/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/event/package-info.java @@ -2,9 +2,7 @@ * Support classes for application events, like standard context events. * To be supported by all major application context implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.event; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java index 08673db5e6be..901486b982c8 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java +++ b/spring-context/src/main/java/org/springframework/context/expression/AnnotatedElementKey.java @@ -18,7 +18,8 @@ import java.lang.reflect.AnnotatedElement; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -35,8 +36,7 @@ public final class AnnotatedElementKey implements Comparable targetClass; + private final @Nullable Class targetClass; /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java index 961f79de3fcf..adb4d12682d1 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/expression/BeanExpressionContextAccessor.java @@ -16,12 +16,13 @@ package org.springframework.context.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java index 2f7f162d1047..ef5a6e0079e1 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/expression/BeanFactoryAccessor.java @@ -16,12 +16,13 @@ package org.springframework.context.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java index 8cdea248a654..ba52e13b956f 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java +++ b/spring-context/src/main/java/org/springframework/context/expression/CachedExpressionEvaluator.java @@ -18,11 +18,12 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.Expression; import org.springframework.expression.spel.standard.SpelExpressionParser; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java index 7b7fda745287..aca1b2085e62 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/expression/EnvironmentAccessor.java @@ -16,12 +16,13 @@ package org.springframework.context.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.Environment; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.PropertyAccessor; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java b/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java index e630c8cc270f..38053c25066f 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/expression/MapAccessor.java @@ -18,6 +18,8 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; @@ -25,7 +27,6 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.CompilablePropertyAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java index c98d60fa595e..18c81c48660f 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java +++ b/spring-context/src/main/java/org/springframework/context/expression/MethodBasedEvaluationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,10 +19,11 @@ import java.lang.reflect.Method; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.core.KotlinDetector; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -45,14 +46,14 @@ public class MethodBasedEvaluationContext extends StandardEvaluationContext { private final Method method; - private final Object[] arguments; + private final @Nullable Object[] arguments; private final ParameterNameDiscoverer parameterNameDiscoverer; private boolean argumentsLoaded = false; - public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] arguments, + public MethodBasedEvaluationContext(Object rootObject, Method method, @Nullable Object[] arguments, ParameterNameDiscoverer parameterNameDiscoverer) { super(rootObject); @@ -64,8 +65,7 @@ public MethodBasedEvaluationContext(Object rootObject, Method method, Object[] a @Override - @Nullable - public Object lookupVariable(String name) { + public @Nullable Object lookupVariable(String name) { Object variable = super.lookupVariable(name); if (variable != null) { return variable; @@ -88,7 +88,7 @@ protected void lazyLoadArguments() { } // Expose indexed variables as well as parameter names (if discoverable) - String[] paramNames = this.parameterNameDiscoverer.getParameterNames(this.method); + @Nullable String[] paramNames = this.parameterNameDiscoverer.getParameterNames(this.method); int paramCount = (paramNames != null ? paramNames.length : this.method.getParameterCount()); int argsCount = this.arguments.length; diff --git a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java index 8b03a9f25d65..bc9d98a0df13 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.java +++ b/spring-context/src/main/java/org/springframework/context/expression/StandardBeanExpressionResolver.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,6 +19,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanExpressionException; import org.springframework.beans.factory.config.BeanExpressionContext; @@ -36,7 +38,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.expression.spel.support.StandardTypeLocator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -155,8 +156,7 @@ public void setExpressionParser(ExpressionParser expressionParser) { @Override - @Nullable - public Object evaluate(@Nullable String value, BeanExpressionContext beanExpressionContext) throws BeansException { + public @Nullable Object evaluate(@Nullable String value, BeanExpressionContext beanExpressionContext) throws BeansException { if (!StringUtils.hasLength(value)) { return value; } @@ -201,8 +201,8 @@ private static int retrieveMaxExpressionLength() { try { int maxLength = Integer.parseInt(value.trim()); - Assert.isTrue(maxLength > 0, () -> "Value [" + maxLength + "] for system property [" - + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME + "] must be positive"); + Assert.isTrue(maxLength > 0, () -> "Value [" + maxLength + "] for system property [" + + MAX_SPEL_EXPRESSION_LENGTH_PROPERTY_NAME + "] must be positive"); return maxLength; } catch (NumberFormatException ex) { diff --git a/spring-context/src/main/java/org/springframework/context/expression/package-info.java b/spring-context/src/main/java/org/springframework/context/expression/package-info.java index f08c49a0e63f..cca8a5d23798 100644 --- a/spring-context/src/main/java/org/springframework/context/expression/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/expression/package-info.java @@ -1,9 +1,7 @@ /** * Expression parsing support within a Spring application context. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.expression; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java index f10305c831ac..5d5bb2b87eb5 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContext.java @@ -18,7 +18,7 @@ import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface for determining the current Locale. @@ -38,7 +38,6 @@ public interface LocaleContext { * depending on the implementation strategy. * @return the current Locale, or {@code null} if no specific Locale associated */ - @Nullable - Locale getLocale(); + @Nullable Locale getLocale(); } diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java index 97a2a2f1ec41..e54c5d657264 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextHolder.java @@ -19,9 +19,10 @@ import java.util.Locale; import java.util.TimeZone; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedInheritableThreadLocal; import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; /** * Simple holder class that associates a LocaleContext instance @@ -51,12 +52,10 @@ public final class LocaleContextHolder { new NamedInheritableThreadLocal<>("LocaleContext"); // Shared default locale at the framework level - @Nullable - private static Locale defaultLocale; + private static @Nullable Locale defaultLocale; // Shared default time zone at the framework level - @Nullable - private static TimeZone defaultTimeZone; + private static @Nullable TimeZone defaultTimeZone; private LocaleContextHolder() { @@ -116,8 +115,7 @@ public static void setLocaleContext(@Nullable LocaleContext localeContext, boole * Return the LocaleContext associated with the current thread, if any. * @return the current LocaleContext, or {@code null} if none */ - @Nullable - public static LocaleContext getLocaleContext() { + public static @Nullable LocaleContext getLocaleContext() { LocaleContext localeContext = localeContextHolder.get(); if (localeContext == null) { localeContext = inheritableLocaleContextHolder.get(); diff --git a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java index 5d31165db814..769a14ad4cc0 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/LocaleContextThreadLocalAccessor.java @@ -17,8 +17,7 @@ package org.springframework.context.i18n; import io.micrometer.context.ThreadLocalAccessor; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Adapt {@link LocaleContextHolder} to the {@link ThreadLocalAccessor} contract @@ -42,8 +41,7 @@ public Object key() { } @Override - @Nullable - public LocaleContext getValue() { + public @Nullable LocaleContext getValue() { return LocaleContextHolder.getLocaleContext(); } diff --git a/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java index 0e678f91c858..7a09831ff345 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/SimpleLocaleContext.java @@ -18,7 +18,7 @@ import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple implementation of the {@link LocaleContext} interface, @@ -32,8 +32,7 @@ */ public class SimpleLocaleContext implements LocaleContext { - @Nullable - private final Locale locale; + private final @Nullable Locale locale; /** @@ -46,8 +45,7 @@ public SimpleLocaleContext(@Nullable Locale locale) { } @Override - @Nullable - public Locale getLocale() { + public @Nullable Locale getLocale() { return this.locale; } diff --git a/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java index 404c7cf90d21..fcc5b8e55d5c 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/SimpleTimeZoneAwareLocaleContext.java @@ -19,7 +19,7 @@ import java.util.Locale; import java.util.TimeZone; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple implementation of the {@link TimeZoneAwareLocaleContext} interface, @@ -36,8 +36,7 @@ */ public class SimpleTimeZoneAwareLocaleContext extends SimpleLocaleContext implements TimeZoneAwareLocaleContext { - @Nullable - private final TimeZone timeZone; + private final @Nullable TimeZone timeZone; /** @@ -54,8 +53,7 @@ public SimpleTimeZoneAwareLocaleContext(@Nullable Locale locale, @Nullable TimeZ @Override - @Nullable - public TimeZone getTimeZone() { + public @Nullable TimeZone getTimeZone() { return this.timeZone; } diff --git a/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java b/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java index ab93b39b7f0a..79173f9d8b4a 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/TimeZoneAwareLocaleContext.java @@ -18,7 +18,7 @@ import java.util.TimeZone; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Extension of {@link LocaleContext}, adding awareness of the current time zone. @@ -39,7 +39,6 @@ public interface TimeZoneAwareLocaleContext extends LocaleContext { * depending on the implementation strategy. * @return the current TimeZone, or {@code null} if no specific TimeZone associated */ - @Nullable - TimeZone getTimeZone(); + @Nullable TimeZone getTimeZone(); } diff --git a/spring-context/src/main/java/org/springframework/context/i18n/package-info.java b/spring-context/src/main/java/org/springframework/context/i18n/package-info.java index d7eba0bb7f23..8dfd46a24e5e 100644 --- a/spring-context/src/main/java/org/springframework/context/i18n/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/i18n/package-info.java @@ -2,9 +2,7 @@ * Abstraction for determining the current Locale, * plus global holder that exposes a thread-bound Locale. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.i18n; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java index 194c96df705b..b83b6fae8d6a 100644 --- a/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java +++ b/spring-context/src/main/java/org/springframework/context/index/CandidateComponentsIndexLoader.java @@ -26,11 +26,11 @@ 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.io.UrlResource; import org.springframework.core.io.support.PropertiesLoaderUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; /** @@ -83,8 +83,7 @@ private CandidateComponentsIndexLoader() { * @throws IllegalArgumentException if any module index cannot * be loaded or if an error occurs while creating {@link CandidateComponentsIndex} */ - @Nullable - public static CandidateComponentsIndex loadIndex(@Nullable ClassLoader classLoader) { + public static @Nullable CandidateComponentsIndex loadIndex(@Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = classLoader; if (classLoaderToUse == null) { classLoaderToUse = CandidateComponentsIndexLoader.class.getClassLoader(); @@ -92,8 +91,7 @@ public static CandidateComponentsIndex loadIndex(@Nullable ClassLoader classLoad return cache.computeIfAbsent(classLoaderToUse, CandidateComponentsIndexLoader::doLoadIndex); } - @Nullable - private static CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) { + private static @Nullable CandidateComponentsIndex doLoadIndex(ClassLoader classLoader) { if (shouldIgnoreIndex) { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/index/package-info.java b/spring-context/src/main/java/org/springframework/context/index/package-info.java index e07328eca199..b036b1d3242f 100644 --- a/spring-context/src/main/java/org/springframework/context/index/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/index/package-info.java @@ -1,9 +1,7 @@ /** * Support package for reading and managing the components index. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.index; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/package-info.java b/spring-context/src/main/java/org/springframework/context/package-info.java index 9aae0c27c845..ca71120d16e9 100644 --- a/spring-context/src/main/java/org/springframework/context/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/package-info.java @@ -10,9 +10,7 @@ * is that application objects can often be configured without * any dependency on Spring-specific APIs. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java index 1ed265a35f8c..c1c8bd322b19 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractApplicationContext.java @@ -33,6 +33,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.CachedIntrospectionResults; @@ -86,7 +87,6 @@ import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -189,12 +189,10 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader private String displayName = ObjectUtils.identityToString(this); /** Parent context. */ - @Nullable - private ApplicationContext parent; + private @Nullable ApplicationContext parent; /** Environment used by this context. */ - @Nullable - private ConfigurableEnvironment environment; + private @Nullable ConfigurableEnvironment environment; /** BeanFactoryPostProcessors to apply on refresh. */ private final List beanFactoryPostProcessors = new ArrayList<>(); @@ -212,27 +210,22 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader private final Lock startupShutdownLock = new ReentrantLock(); /** Currently active startup/shutdown thread. */ - @Nullable - private volatile Thread startupShutdownThread; + private volatile @Nullable Thread startupShutdownThread; /** Reference to the JVM shutdown hook, if registered. */ - @Nullable - private Thread shutdownHook; + private @Nullable Thread shutdownHook; /** ResourcePatternResolver used by this context. */ private final ResourcePatternResolver resourcePatternResolver; /** LifecycleProcessor for managing the lifecycle of beans within this context. */ - @Nullable - private LifecycleProcessor lifecycleProcessor; + private @Nullable LifecycleProcessor lifecycleProcessor; /** MessageSource we delegate our implementation of this interface to. */ - @Nullable - private MessageSource messageSource; + private @Nullable MessageSource messageSource; /** Helper class used in event publishing. */ - @Nullable - private ApplicationEventMulticaster applicationEventMulticaster; + private @Nullable ApplicationEventMulticaster applicationEventMulticaster; /** Application startup metrics. */ private ApplicationStartup applicationStartup = ApplicationStartup.DEFAULT; @@ -241,12 +234,10 @@ public abstract class AbstractApplicationContext extends DefaultResourceLoader private final Set> applicationListeners = new LinkedHashSet<>(); /** Local listeners registered before refresh. */ - @Nullable - private Set> earlyApplicationListeners; + private @Nullable Set> earlyApplicationListeners; /** ApplicationEvents published before the multicaster setup. */ - @Nullable - private Set earlyApplicationEvents; + private @Nullable Set earlyApplicationEvents; /** @@ -315,8 +306,7 @@ public String getDisplayName() { * (that is, this context is the root of the context hierarchy). */ @Override - @Nullable - public ApplicationContext getParent() { + public @Nullable ApplicationContext getParent() { return this.parent; } @@ -1279,7 +1269,7 @@ public T getBean(String name, Class requiredType) throws BeansException { } @Override - public Object getBean(String name, Object... args) throws BeansException { + public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException { assertBeanFactoryActive(); return getBeanFactory().getBean(name, args); } @@ -1291,7 +1281,7 @@ public T getBean(Class requiredType) throws BeansException { } @Override - public T getBean(Class requiredType, Object... args) throws BeansException { + public T getBean(Class requiredType, @Nullable Object @Nullable ... args) throws BeansException { assertBeanFactoryActive(); return getBeanFactory().getBean(requiredType, args); } @@ -1338,15 +1328,13 @@ public boolean isTypeMatch(String name, Class typeToMatch) throws NoSuchBeanD } @Override - @Nullable - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { assertBeanFactoryActive(); return getBeanFactory().getType(name); } @Override - @Nullable - public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { assertBeanFactoryActive(); return getBeanFactory().getType(name, allowFactoryBeanInit); } @@ -1441,8 +1429,7 @@ public Map getBeansWithAnnotation(Class an } @Override - @Nullable - public A findAnnotationOnBean(String beanName, Class annotationType) + public @Nullable A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException { assertBeanFactoryActive(); @@ -1450,8 +1437,7 @@ public A findAnnotationOnBean(String beanName, Class a } @Override - @Nullable - public A findAnnotationOnBean( + public @Nullable A findAnnotationOnBean( String beanName, Class annotationType, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { @@ -1474,8 +1460,7 @@ public Set findAllAnnotationsOnBean( //--------------------------------------------------------------------- @Override - @Nullable - public BeanFactory getParentBeanFactory() { + public @Nullable BeanFactory getParentBeanFactory() { return getParent(); } @@ -1489,8 +1474,7 @@ public boolean containsLocalBean(String name) { * ConfigurableApplicationContext; else, return the parent context itself. * @see org.springframework.context.ConfigurableApplicationContext#getBeanFactory */ - @Nullable - protected BeanFactory getInternalParentBeanFactory() { + protected @Nullable BeanFactory getInternalParentBeanFactory() { return (getParent() instanceof ConfigurableApplicationContext cac ? cac.getBeanFactory() : getParent()); } @@ -1501,13 +1485,12 @@ protected BeanFactory getInternalParentBeanFactory() { //--------------------------------------------------------------------- @Override - @Nullable - public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { return getMessageSource().getMessage(code, args, defaultMessage, locale); } @Override - public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + public String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { return getMessageSource().getMessage(code, args, locale); } @@ -1533,8 +1516,7 @@ private MessageSource getMessageSource() throws IllegalStateException { * Return the internal message source of the parent context if it is an * AbstractApplicationContext too; else, return the parent context itself. */ - @Nullable - protected MessageSource getInternalParentMessageSource() { + protected @Nullable MessageSource getInternalParentMessageSource() { return (getParent() instanceof AbstractApplicationContext abstractApplicationContext ? abstractApplicationContext.messageSource : getParent()); } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java index cb51fe9def12..c483820ac195 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractMessageSource.java @@ -22,11 +22,12 @@ import java.util.Locale; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -64,11 +65,9 @@ */ public abstract class AbstractMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { - @Nullable - private MessageSource parentMessageSource; + private @Nullable MessageSource parentMessageSource; - @Nullable - private Properties commonMessages; + private @Nullable Properties commonMessages; private boolean useCodeAsDefaultMessage = false; @@ -79,8 +78,7 @@ public void setParentMessageSource(@Nullable MessageSource parent) { } @Override - @Nullable - public MessageSource getParentMessageSource() { + public @Nullable MessageSource getParentMessageSource() { return this.parentMessageSource; } @@ -97,8 +95,7 @@ public void setCommonMessages(@Nullable Properties commonMessages) { /** * Return a Properties object defining locale-independent common messages, if any. */ - @Nullable - protected Properties getCommonMessages() { + protected @Nullable Properties getCommonMessages() { return this.commonMessages; } @@ -137,8 +134,7 @@ protected boolean isUseCodeAsDefaultMessage() { @Override - @Nullable - public final String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + public final @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; @@ -150,7 +146,7 @@ public final String getMessage(String code, @Nullable Object[] args, @Nullable S } @Override - public final String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + public final String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { String msg = getMessageInternal(code, args, locale); if (msg != null) { return msg; @@ -195,8 +191,7 @@ public final String getMessage(MessageSourceResolvable resolvable, Locale locale * @see #getMessage(MessageSourceResolvable, Locale) * @see #setUseCodeAsDefaultMessage */ - @Nullable - protected String getMessageInternal(@Nullable String code, @Nullable Object[] args, @Nullable Locale locale) { + protected @Nullable String getMessageInternal(@Nullable String code, Object @Nullable [] args, @Nullable Locale locale) { if (code == null) { return null; } @@ -252,8 +247,7 @@ protected String getMessageInternal(@Nullable String code, @Nullable Object[] ar * @return the resolved message, or {@code null} if not found * @see #getParentMessageSource() */ - @Nullable - protected String getMessageFromParent(String code, @Nullable Object[] args, Locale locale) { + protected @Nullable String getMessageFromParent(String code, Object @Nullable [] args, Locale locale) { MessageSource parent = getParentMessageSource(); if (parent != null) { if (parent instanceof AbstractMessageSource abstractMessageSource) { @@ -283,8 +277,7 @@ protected String getMessageFromParent(String code, @Nullable Object[] args, Loca * @see #renderDefaultMessage(String, Object[], Locale) * @see #getDefaultMessage(String) */ - @Nullable - protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) { + protected @Nullable String getDefaultMessage(MessageSourceResolvable resolvable, Locale locale) { String defaultMessage = resolvable.getDefaultMessage(); String[] codes = resolvable.getCodes(); if (defaultMessage != null) { @@ -313,8 +306,7 @@ protected String getDefaultMessage(MessageSourceResolvable resolvable, Locale lo * @return the default message to use, or {@code null} if none * @see #setUseCodeAsDefaultMessage */ - @Nullable - protected String getDefaultMessage(String code) { + protected @Nullable String getDefaultMessage(String code) { if (isUseCodeAsDefaultMessage()) { return code; } @@ -331,7 +323,7 @@ protected String getDefaultMessage(String code) { * @return an array of arguments with any MessageSourceResolvables resolved */ @Override - protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) { + protected Object[] resolveArguments(Object @Nullable [] args, Locale locale) { if (ObjectUtils.isEmpty(args)) { return super.resolveArguments(args, locale); } @@ -364,8 +356,7 @@ protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) { * @see #resolveCode * @see java.text.MessageFormat */ - @Nullable - protected String resolveCodeWithoutArguments(String code, Locale locale) { + protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) { MessageFormat messageFormat = resolveCode(code, locale); if (messageFormat != null) { synchronized (messageFormat) { @@ -388,7 +379,6 @@ protected String resolveCodeWithoutArguments(String code, Locale locale) { * @return the MessageFormat for the message, or {@code null} if not found * @see #resolveCodeWithoutArguments(String, java.util.Locale) */ - @Nullable - protected abstract MessageFormat resolveCode(String code, Locale locale); + protected abstract @Nullable MessageFormat resolveCode(String code, Locale locale); } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java index 5acf9148f2f8..30d06cc26b48 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableApplicationContext.java @@ -18,12 +18,13 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextException; -import org.springframework.lang.Nullable; /** * Base class for {@link org.springframework.context.ApplicationContext} @@ -64,15 +65,12 @@ */ public abstract class AbstractRefreshableApplicationContext extends AbstractApplicationContext { - @Nullable - private Boolean allowBeanDefinitionOverriding; + private @Nullable Boolean allowBeanDefinitionOverriding; - @Nullable - private Boolean allowCircularReferences; + private @Nullable Boolean allowCircularReferences; /** Bean factory for this context. */ - @Nullable - private volatile DefaultListableBeanFactory beanFactory; + private volatile @Nullable DefaultListableBeanFactory beanFactory; /** diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java index 901e7152303a..ac63bcd5c505 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractRefreshableConfigApplicationContext.java @@ -16,10 +16,11 @@ package org.springframework.context.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -39,8 +40,7 @@ public abstract class AbstractRefreshableConfigApplicationContext extends AbstractRefreshableApplicationContext implements BeanNameAware, InitializingBean { - @Nullable - private String[] configLocations; + private String @Nullable [] configLocations; private boolean setIdCalled = false; @@ -73,7 +73,7 @@ public void setConfigLocation(String location) { * Set the config locations for this application context. *

If not set, the implementation may use a default as appropriate. */ - public void setConfigLocations(@Nullable String... locations) { + public void setConfigLocations(String @Nullable ... locations) { if (locations != null) { Assert.noNullElements(locations, "Config locations must not be null"); this.configLocations = new String[locations.length]; @@ -96,8 +96,7 @@ public void setConfigLocations(@Nullable String... locations) { * @see #getResources * @see #getResourcePatternResolver */ - @Nullable - protected String[] getConfigLocations() { + protected String @Nullable [] getConfigLocations() { return (this.configLocations != null ? this.configLocations : getDefaultConfigLocations()); } @@ -109,8 +108,7 @@ protected String[] getConfigLocations() { * @return an array of default config locations, if any * @see #setConfigLocations */ - @Nullable - protected String[] getDefaultConfigLocations() { + protected String @Nullable [] getDefaultConfigLocations() { return null; } 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 0a0f8bd0be35..310576f4ccb6 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. @@ -20,7 +20,8 @@ import java.util.Locale; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -39,13 +40,11 @@ public abstract class AbstractResourceBasedMessageSource extends AbstractMessage private final Set basenameSet = new LinkedHashSet<>(4); - @Nullable - private String defaultEncoding; + private @Nullable String defaultEncoding; private boolean fallbackToSystemLocale = true; - @Nullable - private Locale defaultLocale; + private @Nullable Locale defaultLocale; private long cacheMillis = -1; @@ -134,8 +133,7 @@ public void setDefaultEncoding(@Nullable String defaultEncoding) { * Return the default charset to use for parsing properties files, if any. * @since 4.3 */ - @Nullable - protected String getDefaultEncoding() { + protected @Nullable String getDefaultEncoding() { return this.defaultEncoding; } @@ -158,9 +156,9 @@ public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { * 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; } @@ -187,8 +185,7 @@ public void setDefaultLocale(@Nullable Locale defaultLocale) { * @see #setFallbackToSystemLocale * @see Locale#getDefault() */ - @Nullable - protected Locale getDefaultLocale() { + protected @Nullable Locale getDefaultLocale() { if (this.defaultLocale != null) { return this.defaultLocale; } diff --git a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java index 961704adcfa9..372f9bc939f6 100644 --- a/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/AbstractXmlApplicationContext.java @@ -18,6 +18,8 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.BeanDefinitionDocumentReader; @@ -25,7 +27,6 @@ import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Convenient base class for {@link org.springframework.context.ApplicationContext} @@ -139,8 +140,7 @@ protected void loadBeanDefinitions(XmlBeanDefinitionReader reader) throws BeansE * @return an array of Resource objects, or {@code null} if none * @see #getConfigLocations() */ - @Nullable - protected Resource[] getConfigResources() { + protected Resource @Nullable [] getConfigResources() { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java index 1da55ce8fe50..7e1124ead0b1 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationContextAwareProcessor.java @@ -16,6 +16,8 @@ package org.springframework.context.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.Aware; import org.springframework.beans.factory.config.BeanPostProcessor; @@ -28,7 +30,6 @@ import org.springframework.context.EnvironmentAware; import org.springframework.context.MessageSourceAware; import org.springframework.context.ResourceLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; /** @@ -79,8 +80,7 @@ public ApplicationContextAwareProcessor(ConfigurableApplicationContext applicati @Override - @Nullable - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof Aware) { invokeAwareInterfaces(bean); } diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java index 342839892a8f..62a3765f73fa 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationListenerDetector.java @@ -22,13 +22,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.DestructionAwareBeanPostProcessor; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationListener; import org.springframework.context.event.ApplicationEventMulticaster; -import org.springframework.lang.Nullable; /** * {@code BeanPostProcessor} that detects beans which implement the {@code ApplicationListener} diff --git a/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java b/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java index 73d37a4aa261..44fbdce7b2b1 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java +++ b/spring-context/src/main/java/org/springframework/context/support/ApplicationObjectSupport.java @@ -18,12 +18,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -52,12 +52,10 @@ public abstract class ApplicationObjectSupport implements ApplicationContextAwar protected final Log logger = LogFactory.getLog(getClass()); /** ApplicationContext this object runs in. */ - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; /** MessageSourceAccessor for easy message access. */ - @Nullable - private MessageSourceAccessor messageSourceAccessor; + private @Nullable MessageSourceAccessor messageSourceAccessor; @Override @@ -140,8 +138,7 @@ protected void initApplicationContext() throws BeansException { * Return the ApplicationContext that this object is associated with. * @throws IllegalStateException if not running in an ApplicationContext */ - @Nullable - public final ApplicationContext getApplicationContext() throws IllegalStateException { + public final @Nullable ApplicationContext getApplicationContext() throws IllegalStateException { if (this.applicationContext == null && isContextRequired()) { throw new IllegalStateException( "ApplicationObjectSupport instance [" + this + "] does not run in an ApplicationContext"); @@ -166,8 +163,7 @@ protected final ApplicationContext obtainApplicationContext() { * used by this object, for easy message access. * @throws IllegalStateException if not running in an ApplicationContext */ - @Nullable - protected final MessageSourceAccessor getMessageSourceAccessor() throws IllegalStateException { + protected final @Nullable MessageSourceAccessor getMessageSourceAccessor() throws IllegalStateException { if (this.messageSourceAccessor == null && isContextRequired()) { throw new IllegalStateException( "ApplicationObjectSupport instance [" + this + "] does not run in an ApplicationContext"); diff --git a/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java index 57fa66128529..bdb2736f3581 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/ClassPathXmlApplicationContext.java @@ -16,11 +16,12 @@ package org.springframework.context.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -51,8 +52,7 @@ */ public class ClassPathXmlApplicationContext extends AbstractXmlApplicationContext { - @Nullable - private Resource[] configResources; + private Resource @Nullable [] configResources; /** @@ -204,8 +204,7 @@ public ClassPathXmlApplicationContext(String[] paths, Class clazz, @Nullable @Override - @Nullable - protected Resource[] getConfigResources() { + protected Resource @Nullable [] getConfigResources() { return this.configResources; } diff --git a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java index 525d916ca0c5..531feb00c9db 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java +++ b/spring-context/src/main/java/org/springframework/context/support/ContextTypeMatchClassLoader.java @@ -22,11 +22,11 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.DecoratingClassLoader; import org.springframework.core.OverridingClassLoader; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -47,8 +47,7 @@ class ContextTypeMatchClassLoader extends DecoratingClassLoader implements Smart } - @Nullable - private static final Method findLoadedClassMethod; + private static final @Nullable Method findLoadedClassMethod; static { // Try to enable findLoadedClass optimization which allows us to selectively @@ -123,8 +122,7 @@ protected boolean isEligibleForOverriding(String className) { } @Override - @Nullable - protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + protected @Nullable Class loadClassForOverriding(String name) throws ClassNotFoundException { byte[] bytes = bytesCache.get(name); if (bytes == null) { bytes = loadBytesForClass(name); diff --git a/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java b/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java index f04f33d2fc44..3ca93ab825c7 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/context/support/ConversionServiceFactoryBean.java @@ -18,13 +18,14 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.ConversionServiceFactory; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.GenericConversionService; -import org.springframework.lang.Nullable; /** * A factory providing convenient access to a ConversionService configured with @@ -50,11 +51,9 @@ */ public class ConversionServiceFactoryBean implements FactoryBean, InitializingBean { - @Nullable - private Set converters; + private @Nullable Set converters; - @Nullable - private GenericConversionService conversionService; + private @Nullable GenericConversionService conversionService; /** @@ -87,8 +86,7 @@ protected GenericConversionService createConversionService() { // implementing FactoryBean @Override - @Nullable - public ConversionService getObject() { + public @Nullable ConversionService getObject() { return this.conversionService; } 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 e9a918cfc6d5..fca08a51132f 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.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; @@ -37,6 +40,7 @@ import org.crac.Core; import org.crac.RestoreException; import org.crac.management.CRaCMXBean; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -49,9 +53,9 @@ import org.springframework.context.SmartLifecycle; import org.springframework.core.NativeDetector; import org.springframework.core.SpringProperties; -import org.springframework.lang.Nullable; 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 @@ * 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,21 +117,20 @@ 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; private volatile boolean running; - @Nullable - private volatile ConfigurableListableBeanFactory beanFactory; + private volatile @Nullable ConfigurableListableBeanFactory beanFactory; - @Nullable - private volatile Set stoppedBeans; + private volatile @Nullable Set stoppedBeans; // Just for keeping a strong reference to the registered CRaC Resource, if any - @Nullable - private Object cracResource; + private @Nullable Object cracResource; public DefaultLifecycleProcessor() { @@ -130,20 +144,59 @@ else if (checkpointOnRefresh) { } + /** + * 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); } /** @@ -171,17 +224,15 @@ public void setTimeoutPerShutdownPhase(long timeoutPerShutdownPhase) { 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; } @@ -191,6 +242,21 @@ private ConfigurableListableBeanFactory getBeanFactory() { 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 @@ -285,9 +351,8 @@ private void startBeans(boolean autoStartupOnly) { 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); } }); @@ -308,30 +373,41 @@ private boolean isAutoStartupCandidate(String beanName, Lifecycle bean) { * @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() + "]"); - } - try { - bean.start(); - } - catch (Throwable ex) { - throw new ApplicationContextException("Failed to start bean '" + beanName + "'", ex); + if (futures != null) { + futures.add(CompletableFuture.runAsync(() -> doStart(beanName, bean), getBootstrapExecutor())); } - 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) : @@ -344,9 +420,8 @@ private void stopBeans() { 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()) { @@ -417,7 +492,7 @@ else if (bean instanceof SmartLifecycle) { } - // overridable hooks + // Overridable hooks /** * Retrieve all applicable Lifecycle beans: all singletons that have already been created, @@ -473,8 +548,6 @@ private class LifecycleGroup { private final int phase; - private final long timeout; - private final Map lifecycleBeans; private final boolean autoStartupOnly; @@ -483,11 +556,8 @@ private class LifecycleGroup { 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; } @@ -506,8 +576,26 @@ public void start() { 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); + } } } @@ -531,11 +619,14 @@ else if (member.bean instanceof SmartLifecycle) { } } try { - latch.await(this.timeout, TimeUnit.MILLISECONDS); - if (latch.getCount() > 0 && !countDownBeanNames.isEmpty() && logger.isInfoEnabled()) { - logger.info("Shutdown phase " + this.phase + " ends with " + countDownBeanNames.size() + - " bean" + (countDownBeanNames.size() > 1 ? "s" : "") + - " still running after timeout of " + this.timeout + "ms: " + countDownBeanNames); + 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) { @@ -592,8 +683,7 @@ public void checkpointRestore() { */ private class CracResourceAdapter implements org.crac.Resource { - @Nullable - private CyclicBarrier barrier; + private @Nullable CyclicBarrier barrier; @Override public void beforeCheckpoint(org.crac.Context context) { diff --git a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java index df09bc8957f9..1d0bf70ae474 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java +++ b/spring-context/src/main/java/org/springframework/context/support/DefaultMessageSourceResolvable.java @@ -18,8 +18,9 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.context.MessageSourceResolvable; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -35,14 +36,11 @@ @SuppressWarnings("serial") public class DefaultMessageSourceResolvable implements MessageSourceResolvable, Serializable { - @Nullable - private final String[] codes; + private final String @Nullable [] codes; - @Nullable - private final Object[] arguments; + private final Object @Nullable [] arguments; - @Nullable - private final String defaultMessage; + private final @Nullable String defaultMessage; /** @@ -86,7 +84,7 @@ public DefaultMessageSourceResolvable(String[] codes, Object[] arguments) { * @param defaultMessage the default message to be used to resolve this message */ public DefaultMessageSourceResolvable( - @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + String @Nullable [] codes, Object @Nullable [] arguments, @Nullable String defaultMessage) { this.codes = codes; this.arguments = arguments; @@ -106,26 +104,22 @@ public DefaultMessageSourceResolvable(MessageSourceResolvable resolvable) { * Return the default code of this resolvable, that is, * the last one in the codes array. */ - @Nullable - public String getCode() { + public @Nullable String getCode() { return (this.codes != null && this.codes.length > 0 ? this.codes[this.codes.length - 1] : null); } @Override - @Nullable - public String[] getCodes() { + public String @Nullable [] getCodes() { return this.codes; } @Override - @Nullable - public Object[] getArguments() { + public Object @Nullable [] getArguments() { return this.arguments; } @Override - @Nullable - public String getDefaultMessage() { + public @Nullable String getDefaultMessage() { return this.defaultMessage; } diff --git a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java index afe4db3e89b0..23359724f6fb 100644 --- a/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/DelegatingMessageSource.java @@ -18,11 +18,12 @@ import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.context.HierarchicalMessageSource; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; -import org.springframework.lang.Nullable; /** * Empty {@link MessageSource} that delegates all calls to the parent MessageSource. @@ -37,8 +38,7 @@ */ public class DelegatingMessageSource extends MessageSourceSupport implements HierarchicalMessageSource { - @Nullable - private MessageSource parentMessageSource; + private @Nullable MessageSource parentMessageSource; @Override @@ -47,15 +47,13 @@ public void setParentMessageSource(@Nullable MessageSource parent) { } @Override - @Nullable - public MessageSource getParentMessageSource() { + public @Nullable MessageSource getParentMessageSource() { return this.parentMessageSource; } @Override - @Nullable - public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { if (this.parentMessageSource != null) { return this.parentMessageSource.getMessage(code, args, defaultMessage, locale); } @@ -68,7 +66,7 @@ else if (defaultMessage != null) { } @Override - public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + public String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { if (this.parentMessageSource != null) { return this.parentMessageSource.getMessage(code, args, locale); } diff --git a/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java b/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java index dc85006c5d87..6f9948e7dc92 100644 --- a/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java +++ b/spring-context/src/main/java/org/springframework/context/support/EmbeddedValueResolutionSupport.java @@ -16,8 +16,9 @@ package org.springframework.context.support; +import org.jspecify.annotations.Nullable; + import org.springframework.context.EmbeddedValueResolverAware; -import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; /** @@ -29,8 +30,7 @@ */ public class EmbeddedValueResolutionSupport implements EmbeddedValueResolverAware { - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; @Override @@ -44,8 +44,7 @@ public void setEmbeddedValueResolver(StringValueResolver resolver) { * @return the resolved value, or always the original value if no resolver is available * @see #setEmbeddedValueResolver */ - @Nullable - protected String resolveEmbeddedValue(String value) { + protected @Nullable String resolveEmbeddedValue(String value) { return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); } diff --git a/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java index c9166da80182..556d077fee29 100644 --- a/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/FileSystemXmlApplicationContext.java @@ -16,11 +16,12 @@ package org.springframework.context.support; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Standalone XML application context, taking the context definition files diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java index f76668f4ebac..5a8ae4b2b9e5 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericApplicationContext.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,11 +23,14 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.support.ClassHintUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanDefinitionStoreException; +import org.springframework.beans.factory.BeanRegistrar; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.BeanDefinition; @@ -36,6 +39,7 @@ import org.springframework.beans.factory.config.SmartInstantiationAwareBeanPostProcessor; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor; +import org.springframework.beans.factory.support.BeanRegistryAdapter; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.MergedBeanDefinitionPostProcessor; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -45,7 +49,6 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.metrics.ApplicationStartup; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -107,8 +110,7 @@ public class GenericApplicationContext extends AbstractApplicationContext implem private final DefaultListableBeanFactory beanFactory; - @Nullable - private ResourceLoader resourceLoader; + private @Nullable ResourceLoader resourceLoader; private boolean customClassLoader = false; @@ -270,8 +272,7 @@ public void setClassLoader(@Nullable ClassLoader classLoader) { } @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { if (this.resourceLoader != null && !this.customClassLoader) { return this.resourceLoader.getClassLoader(); } @@ -492,7 +493,7 @@ private void preDetermineBeanType(String beanName, List void registerBean(Class beanClass, Object... constructorArgs) { + public void registerBean(Class beanClass, @Nullable Object... constructorArgs) { registerBean(null, beanClass, constructorArgs); } @@ -507,7 +508,7 @@ public void registerBean(Class beanClass, Object... constructorArgs) { * (may be {@code null} or empty) * @since 5.2 (since 5.0 on the AnnotationConfigApplicationContext subclass) */ - public void registerBean(@Nullable String beanName, Class beanClass, Object... constructorArgs) { + public void registerBean(@Nullable String beanName, Class beanClass, @Nullable Object... constructorArgs) { registerBean(beanName, beanClass, (Supplier) null, bd -> { for (Object arg : constructorArgs) { @@ -595,6 +596,21 @@ public void registerBean(@Nullable String beanName, Class beanClass, registerBeanDefinition(nameToUse, beanDefinition); } + /** + * Invoke the given registrars for registering their beans with this + * application context. + *

This can be used to apply encapsulated pieces of programmatic + * bean registration to this application context without relying on + * individual calls to its context-level {@code registerBean} methods. + * @param registrars one or more {@link BeanRegistrar} instances + * @since 7.0 + */ + public void register(BeanRegistrar... registrars) { + for (BeanRegistrar registrar : registrars) { + new BeanRegistryAdapter(this.beanFactory, getEnvironment(), registrar.getClass()).register(registrar); + } + } + /** * {@link RootBeanDefinition} subclass for {@code #registerBean} based @@ -612,8 +628,7 @@ public ClassDerivedBeanDefinition(ClassDerivedBeanDefinition original) { } @Override - @Nullable - public Constructor[] getPreferredConstructors() { + public Constructor @Nullable [] getPreferredConstructors() { Constructor[] fromAttribute = super.getPreferredConstructors(); if (fromAttribute != null) { return fromAttribute; diff --git a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java index e164165074d3..355986bbc092 100644 --- a/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/GenericGroovyApplicationContext.java @@ -19,6 +19,7 @@ import groovy.lang.GroovyObject; import groovy.lang.GroovySystem; import groovy.lang.MetaClass; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanWrapper; import org.springframework.beans.BeanWrapperImpl; @@ -28,7 +29,6 @@ import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * An {@link org.springframework.context.ApplicationContext} implementation that extends @@ -251,8 +251,7 @@ public void setProperty(String property, Object newValue) { } @Override - @Nullable - public Object getProperty(String property) { + public @Nullable Object getProperty(String property) { if (containsBean(property)) { return getBean(property); } diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java index ba3f7b7c9b01..508f468d76b1 100644 --- a/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceAccessor.java @@ -18,11 +18,12 @@ import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.NoSuchMessageException; import org.springframework.context.i18n.LocaleContextHolder; -import org.springframework.lang.Nullable; /** * Helper class for easy access to messages from a MessageSource, @@ -39,8 +40,7 @@ public class MessageSourceAccessor { private final MessageSource messageSource; - @Nullable - private final Locale defaultLocale; + private final @Nullable Locale defaultLocale; /** @@ -107,7 +107,7 @@ public String getMessage(String code, String defaultMessage, Locale locale) { * @param defaultMessage the String to return if the lookup fails * @return the message */ - public String getMessage(String code, @Nullable Object[] args, String defaultMessage) { + public String getMessage(String code, Object @Nullable [] args, String defaultMessage) { String msg = this.messageSource.getMessage(code, args, defaultMessage, getDefaultLocale()); return (msg != null ? msg : ""); } @@ -120,7 +120,7 @@ public String getMessage(String code, @Nullable Object[] args, String defaultMes * @param locale the Locale in which to do lookup * @return the message */ - public String getMessage(String code, @Nullable Object[] args, String defaultMessage, Locale locale) { + public String getMessage(String code, Object @Nullable [] args, String defaultMessage, Locale locale) { String msg = this.messageSource.getMessage(code, args, defaultMessage, locale); return (msg != null ? msg : ""); } @@ -153,7 +153,7 @@ public String getMessage(String code, Locale locale) throws NoSuchMessageExcepti * @return the message * @throws org.springframework.context.NoSuchMessageException if not found */ - public String getMessage(String code, @Nullable Object[] args) throws NoSuchMessageException { + public String getMessage(String code, Object @Nullable [] args) throws NoSuchMessageException { return this.messageSource.getMessage(code, args, getDefaultLocale()); } @@ -165,7 +165,7 @@ public String getMessage(String code, @Nullable Object[] args) throws NoSuchMess * @return the message * @throws org.springframework.context.NoSuchMessageException if not found */ - public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + public String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { return this.messageSource.getMessage(code, args, locale); } diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java index b860a38082c7..29fd34b1f344 100644 --- a/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceResourceBundle.java @@ -20,9 +20,10 @@ import java.util.Locale; import java.util.ResourceBundle; +import org.jspecify.annotations.Nullable; + import org.springframework.context.MessageSource; import org.springframework.context.NoSuchMessageException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -71,8 +72,7 @@ public MessageSourceResourceBundle(MessageSource source, Locale locale, Resource * Returns {@code null} if the message could not be resolved. */ @Override - @Nullable - protected Object handleGetObject(String key) { + protected @Nullable Object handleGetObject(String key) { try { return this.messageSource.getMessage(key, null, this.locale); } diff --git a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java index 9aaaf33bcdfc..96b633b4763d 100644 --- a/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java +++ b/spring-context/src/main/java/org/springframework/context/support/MessageSourceSupport.java @@ -23,8 +23,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -98,7 +98,7 @@ protected boolean isAlwaysUseMessageFormat() { * @return the rendered default message (with resolved arguments) * @see #formatMessage(String, Object[], java.util.Locale) */ - protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] args, Locale locale) { + protected String renderDefaultMessage(String defaultMessage, Object @Nullable [] args, Locale locale) { return formatMessage(defaultMessage, args, locale); } @@ -112,7 +112,7 @@ protected String renderDefaultMessage(String defaultMessage, @Nullable Object[] * @param locale the Locale used for formatting * @return the formatted message (with resolved arguments) */ - protected String formatMessage(String msg, @Nullable Object[] args, Locale locale) { + protected String formatMessage(String msg, Object @Nullable [] args, Locale locale) { if (!isAlwaysUseMessageFormat() && ObjectUtils.isEmpty(args)) { return msg; } @@ -158,7 +158,7 @@ protected MessageFormat createMessageFormat(String msg, Locale locale) { * @param locale the Locale to resolve against * @return the resolved argument array */ - protected Object[] resolveArguments(@Nullable Object[] args, Locale locale) { + protected Object[] resolveArguments(Object @Nullable [] args, Locale locale) { return (args != null ? args : new Object[0]); } 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 5d6784191ab7..63951ce330ba 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 @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.PropertyValue; import org.springframework.beans.factory.config.BeanDefinition; @@ -49,7 +50,6 @@ import org.springframework.core.PriorityOrdered; import org.springframework.core.metrics.ApplicationStartup; import org.springframework.core.metrics.StartupStep; -import org.springframework.lang.Nullable; /** * Delegate for AbstractApplicationContext's post-processor handling. @@ -493,8 +493,8 @@ private void postProcessRootBeanDefinition(List postProcessors, BeanDefinitionValueResolver valueResolver, @Nullable Object value) { - if (value instanceof BeanDefinitionHolder bdh - && bdh.getBeanDefinition() instanceof AbstractBeanDefinition innerBd) { + if (value instanceof BeanDefinitionHolder bdh && + bdh.getBeanDefinition() instanceof AbstractBeanDefinition innerBd) { Class innerBeanType = resolveBeanType(innerBd); resolveInnerBeanDefinition(valueResolver, innerBd, (innerBeanName, innerBeanDefinition) 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 31d1652542b8..18b2733b46ee 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. @@ -19,21 +19,22 @@ import java.io.IOException; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; 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; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringValueResolver; @@ -49,7 +50,7 @@ * 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. @@ -80,14 +81,11 @@ public class PropertySourcesPlaceholderConfigurer extends PlaceholderConfigurerS public static final String ENVIRONMENT_PROPERTIES_PROPERTY_SOURCE_NAME = "environmentProperties"; - @Nullable - private MutablePropertySources propertySources; + private @Nullable MutablePropertySources propertySources; - @Nullable - private PropertySources appliedPropertySources; + private @Nullable PropertySources appliedPropertySources; - @Nullable - private Environment environment; + private @Nullable Environment environment; /** @@ -101,8 +99,9 @@ public void setPropertySources(PropertySources propertySources) { } /** - * {@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 */ @@ -132,28 +131,11 @@ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) 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 - @Nullable - public 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 = @@ -176,6 +158,7 @@ public String getProperty(String key) { /** * 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 */ @@ -188,7 +171,7 @@ protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySou * 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); @@ -216,7 +199,7 @@ protected void processProperties(ConfigurableListableBeanFactory beanFactoryToPr * {@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"); @@ -234,4 +217,91 @@ public PropertySources getAppliedPropertySources() throws IllegalStateException 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/context/support/ReloadableResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java index 3f70d049435e..d79fde8e4050 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ReloadableResourceBundleMessageSource.java @@ -31,11 +31,12 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ResourceLoaderAware; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.DefaultPropertiesPersister; @@ -97,8 +98,7 @@ public class ReloadableResourceBundleMessageSource extends AbstractResourceBased private List fileExtensions = List.of(".properties", XML_EXTENSION); - @Nullable - private Properties fileEncodings; + private @Nullable Properties fileEncodings; private boolean concurrentRefresh = true; @@ -191,8 +191,7 @@ public void setResourceLoader(@Nullable ResourceLoader resourceLoader) { * returning the value found in the bundle as-is (without MessageFormat parsing). */ @Override - @Nullable - protected String resolveCodeWithoutArguments(String code, Locale locale) { + protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) { if (getCacheMillis() < 0) { PropertiesHolder propHolder = getMergedProperties(locale); String result = propHolder.getProperty(code); @@ -220,8 +219,7 @@ protected String resolveCodeWithoutArguments(String code, Locale locale) { * using a cached MessageFormat instance per message code. */ @Override - @Nullable - protected MessageFormat resolveCode(String code, Locale locale) { + protected @Nullable MessageFormat resolveCode(String code, Locale locale) { if (getCacheMillis() < 0) { PropertiesHolder propHolder = getMergedProperties(locale); MessageFormat result = propHolder.getMessageFormat(code, locale); @@ -542,8 +540,7 @@ protected PropertiesHolder refreshProperties(String filename, @Nullable Properti * @return the {@code Resource} to use, or {@code null} if none found * @since 6.1 */ - @Nullable - protected Resource resolveResource(String filename) { + protected @Nullable Resource resolveResource(String filename) { for (String fileExtension : this.fileExtensions) { Resource resource = this.resourceLoader.getResource(filename + fileExtension); if (resource.exists()) { @@ -645,8 +642,7 @@ public String toString() { */ protected class PropertiesHolder { - @Nullable - private final Properties properties; + private final @Nullable Properties properties; private final long fileTimestamp; @@ -668,8 +664,7 @@ public PropertiesHolder(Properties properties, long fileTimestamp) { this.fileTimestamp = fileTimestamp; } - @Nullable - public Properties getProperties() { + public @Nullable Properties getProperties() { return this.properties; } @@ -685,16 +680,14 @@ public long getRefreshTimestamp() { return this.refreshTimestamp; } - @Nullable - public String getProperty(String code) { + public @Nullable String getProperty(String code) { if (this.properties == null) { return null; } return this.properties.getProperty(code); } - @Nullable - public MessageFormat getMessageFormat(String code, Locale locale) { + public @Nullable MessageFormat getMessageFormat(String code, Locale locale) { if (this.properties == null) { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java index bc3393ebb698..3b325e831d2a 100644 --- a/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/ResourceBundleMessageSource.java @@ -31,8 +31,9 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -76,11 +77,9 @@ */ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSource implements BeanClassLoaderAware { - @Nullable - private ClassLoader bundleClassLoader; + private @Nullable ClassLoader bundleClassLoader; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** * Cache to hold loaded ResourceBundles. @@ -103,8 +102,7 @@ public class ResourceBundleMessageSource extends AbstractResourceBasedMessageSou private final Map>> cachedBundleMessageFormats = new ConcurrentHashMap<>(); - @Nullable - private volatile MessageSourceControl control = new MessageSourceControl(); + private volatile @Nullable MessageSourceControl control = new MessageSourceControl(); public ResourceBundleMessageSource() { @@ -129,8 +127,7 @@ public void setBundleClassLoader(ClassLoader classLoader) { *

Default is the containing BeanFactory's bean ClassLoader. * @see #setBundleClassLoader */ - @Nullable - protected ClassLoader getBundleClassLoader() { + protected @Nullable ClassLoader getBundleClassLoader() { return (this.bundleClassLoader != null ? this.bundleClassLoader : this.beanClassLoader); } @@ -145,8 +142,7 @@ public void setBeanClassLoader(ClassLoader classLoader) { * returning the value found in the bundle as-is (without MessageFormat parsing). */ @Override - @Nullable - protected String resolveCodeWithoutArguments(String code, Locale locale) { + protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) { Set basenames = getBasenameSet(); for (String basename : basenames) { ResourceBundle bundle = getResourceBundle(basename, locale); @@ -165,8 +161,7 @@ protected String resolveCodeWithoutArguments(String code, Locale locale) { * using a cached MessageFormat instance per message code. */ @Override - @Nullable - protected MessageFormat resolveCode(String code, Locale locale) { + protected @Nullable MessageFormat resolveCode(String code, Locale locale) { Set basenames = getBasenameSet(); for (String basename : basenames) { ResourceBundle bundle = getResourceBundle(basename, locale); @@ -189,8 +184,7 @@ protected MessageFormat resolveCode(String code, Locale locale) { * @return the resulting ResourceBundle, or {@code null} if none * found for the given basename and Locale */ - @Nullable - protected ResourceBundle getResourceBundle(String basename, Locale locale) { + protected @Nullable ResourceBundle getResourceBundle(String basename, Locale locale) { if (getCacheMillis() >= 0) { // Fresh ResourceBundle.getBundle call in order to let ResourceBundle // do its native caching, at the expense of more extensive lookup steps. @@ -311,8 +305,7 @@ protected ResourceBundle loadBundle(InputStream inputStream) throws IOException * defined for the given code * @throws MissingResourceException if thrown by the ResourceBundle */ - @Nullable - protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) + protected @Nullable MessageFormat getMessageFormat(ResourceBundle bundle, String code, Locale locale) throws MissingResourceException { Map> codeMap = this.cachedBundleMessageFormats.get(bundle); @@ -357,8 +350,7 @@ protected MessageFormat getMessageFormat(ResourceBundle bundle, String code, Loc * @see ResourceBundle#getString(String) * @see ResourceBundle#containsKey(String) */ - @Nullable - protected String getStringOrNull(ResourceBundle bundle, String key) { + protected @Nullable String getStringOrNull(ResourceBundle bundle, String key) { if (bundle.containsKey(key)) { try { return bundle.getString(key); @@ -388,8 +380,7 @@ public String toString() { private class MessageSourceControl extends ResourceBundle.Control { @Override - @Nullable - public ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) + public @Nullable ResourceBundle newBundle(String baseName, Locale locale, String format, ClassLoader loader, boolean reload) throws IllegalAccessException, InstantiationException, IOException { // Special handling of default encoding @@ -436,8 +427,7 @@ public ResourceBundle newBundle(String baseName, Locale locale, String format, C } @Override - @Nullable - public Locale getFallbackLocale(String baseName, Locale locale) { + public @Nullable Locale getFallbackLocale(String baseName, Locale locale) { Locale defaultLocale = getDefaultLocale(); return (defaultLocale != null && !defaultLocale.equals(locale) ? defaultLocale : null); } diff --git a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java index 00af013192ef..67bad8cae59b 100644 --- a/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java +++ b/spring-context/src/main/java/org/springframework/context/support/SimpleThreadScope.java @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.config.Scope; import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; /** * A simple thread-backed {@link Scope} implementation. @@ -72,8 +72,7 @@ public Object get(String name, ObjectFactory objectFactory) { } @Override - @Nullable - public Object remove(String name) { + public @Nullable Object remove(String name) { Map scope = this.threadScope.get(); return scope.remove(name); } @@ -85,8 +84,7 @@ public void registerDestructionCallback(String name, Runnable callback) { } @Override - @Nullable - public Object resolveContextualObject(String key) { + public @Nullable Object resolveContextualObject(String key) { return null; } diff --git a/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java b/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java index fc58e3f5031c..2ede88fd5600 100644 --- a/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java +++ b/spring-context/src/main/java/org/springframework/context/support/StaticApplicationContext.java @@ -18,12 +18,13 @@ import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.support.GenericBeanDefinition; import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; /** * {@link org.springframework.context.ApplicationContext} implementation diff --git a/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java b/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java index 9f8ace3ab2fd..44105778d909 100644 --- a/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java +++ b/spring-context/src/main/java/org/springframework/context/support/StaticMessageSource.java @@ -21,7 +21,8 @@ import java.util.Locale; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -40,8 +41,7 @@ public class StaticMessageSource extends AbstractMessageSource { @Override - @Nullable - protected String resolveCodeWithoutArguments(String code, Locale locale) { + protected @Nullable String resolveCodeWithoutArguments(String code, Locale locale) { Map localeMap = this.messageMap.get(code); if (localeMap == null) { return null; @@ -54,8 +54,7 @@ protected String resolveCodeWithoutArguments(String code, Locale locale) { } @Override - @Nullable - protected MessageFormat resolveCode(String code, Locale locale) { + protected @Nullable MessageFormat resolveCode(String code, Locale locale) { Map localeMap = this.messageMap.get(code); if (localeMap == null) { return null; @@ -107,8 +106,7 @@ private class MessageHolder { private final Locale locale; - @Nullable - private volatile MessageFormat cachedFormat; + private volatile @Nullable MessageFormat cachedFormat; public MessageHolder(String message, Locale locale) { this.message = message; diff --git a/spring-context/src/main/java/org/springframework/context/support/package-info.java b/spring-context/src/main/java/org/springframework/context/support/package-info.java index 2ec0ef515332..fabae8e74770 100644 --- a/spring-context/src/main/java/org/springframework/context/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/support/package-info.java @@ -3,9 +3,7 @@ * such as abstract base classes for ApplicationContext * implementations and a MessageSource implementation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java b/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java index db73317d36c3..47a35f93973c 100644 --- a/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java +++ b/spring-context/src/main/java/org/springframework/context/weaving/AspectJWeavingEnabler.java @@ -21,6 +21,7 @@ import java.security.ProtectionDomain; import org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanClassLoaderAware; @@ -29,7 +30,6 @@ import org.springframework.core.Ordered; import org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver; import org.springframework.instrument.classloading.LoadTimeWeaver; -import org.springframework.lang.Nullable; /** * Post-processor that registers AspectJ's @@ -50,11 +50,9 @@ public class AspectJWeavingEnabler public static final String ASPECTJ_AOP_XML_RESOURCE = "META-INF/aop.xml"; - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; - @Nullable - private LoadTimeWeaver loadTimeWeaver; + private @Nullable LoadTimeWeaver loadTimeWeaver; @Override diff --git a/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java index 9a815627fb09..927ce20540fb 100644 --- a/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/context/weaving/DefaultContextLoadTimeWeaver.java @@ -20,6 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.DisposableBean; @@ -30,7 +31,6 @@ import org.springframework.instrument.classloading.glassfish.GlassFishLoadTimeWeaver; import org.springframework.instrument.classloading.jboss.JBossLoadTimeWeaver; import org.springframework.instrument.classloading.tomcat.TomcatLoadTimeWeaver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -57,8 +57,7 @@ public class DefaultContextLoadTimeWeaver implements LoadTimeWeaver, BeanClassLo protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private LoadTimeWeaver loadTimeWeaver; + private @Nullable LoadTimeWeaver loadTimeWeaver; public DefaultContextLoadTimeWeaver() { @@ -104,8 +103,7 @@ else if (InstrumentationLoadTimeWeaver.isInstrumentationAvailable()) { * determining a load-time weaver based on the ClassLoader name alone may * legitimately fail due to other mismatches. */ - @Nullable - protected LoadTimeWeaver createServerSpecificLoadTimeWeaver(ClassLoader classLoader) { + protected @Nullable LoadTimeWeaver createServerSpecificLoadTimeWeaver(ClassLoader classLoader) { String name = classLoader.getClass().getName(); try { if (name.startsWith("org.apache.catalina")) { diff --git a/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java b/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java index bb045de9a68b..2b06c262ff57 100644 --- a/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java +++ b/spring-context/src/main/java/org/springframework/context/weaving/LoadTimeWeaverAwareProcessor.java @@ -16,13 +16,14 @@ package org.springframework.context.weaving; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.instrument.classloading.LoadTimeWeaver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,11 +44,9 @@ */ public class LoadTimeWeaverAwareProcessor implements BeanPostProcessor, BeanFactoryAware { - @Nullable - private LoadTimeWeaver loadTimeWeaver; + private @Nullable LoadTimeWeaver loadTimeWeaver; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-context/src/main/java/org/springframework/context/weaving/package-info.java b/spring-context/src/main/java/org/springframework/context/weaving/package-info.java index 889d99ed2c6a..4dccb6742c9a 100644 --- a/spring-context/src/main/java/org/springframework/context/weaving/package-info.java +++ b/spring-context/src/main/java/org/springframework/context/weaving/package-info.java @@ -2,9 +2,7 @@ * Load-time weaving support for a Spring application context, building on Spring's * {@link org.springframework.instrument.classloading.LoadTimeWeaver} abstraction. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.context.weaving; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/ejb/config/package-info.java b/spring-context/src/main/java/org/springframework/ejb/config/package-info.java index 501900852b51..4439dd32500e 100644 --- a/spring-context/src/main/java/org/springframework/ejb/config/package-info.java +++ b/spring-context/src/main/java/org/springframework/ejb/config/package-info.java @@ -2,9 +2,7 @@ * Support package for EJB/Jakarta EE-related configuration, * with XML schema being the primary configuration format. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.ejb.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java b/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java index 60813fc909b7..a9fb8adae564 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/DurationFormat.java @@ -25,7 +25,7 @@ import java.time.temporal.ChronoUnit; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Declares that a field or method parameter should be formatted as a diff --git a/spring-context/src/main/java/org/springframework/format/annotation/package-info.java b/spring-context/src/main/java/org/springframework/format/annotation/package-info.java index 207316bb12a9..473eac451585 100644 --- a/spring-context/src/main/java/org/springframework/format/annotation/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotations for declaratively configuring field and parameter formatting rules. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java index ff6a5cce22a2..32ba16381dbb 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatter.java @@ -28,10 +28,11 @@ import java.util.Set; import java.util.TimeZone; +import org.jspecify.annotations.Nullable; + import org.springframework.format.Formatter; import org.springframework.format.annotation.DateTimeFormat; import org.springframework.format.annotation.DateTimeFormat.ISO; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -76,25 +77,19 @@ public class DateFormatter implements Formatter { } - @Nullable - private Object source; + private @Nullable Object source; - @Nullable - private String pattern; + private @Nullable String pattern; - @Nullable - private String[] fallbackPatterns; + private String @Nullable [] fallbackPatterns; private int style = DateFormat.DEFAULT; - @Nullable - private String stylePattern; + private @Nullable String stylePattern; - @Nullable - private ISO iso; + private @Nullable ISO iso; - @Nullable - private TimeZone timeZone; + private @Nullable TimeZone timeZone; private boolean lenient = false; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java index 2c165c5c0feb..0e0c37cd1c1f 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/DateFormatterRegistrar.java @@ -19,11 +19,12 @@ import java.util.Calendar; import java.util.Date; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.format.FormatterRegistrar; import org.springframework.format.FormatterRegistry; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,8 +43,7 @@ */ public class DateFormatterRegistrar implements FormatterRegistrar { - @Nullable - private DateFormatter dateFormatter; + private @Nullable DateFormatter dateFormatter; /** diff --git a/spring-context/src/main/java/org/springframework/format/datetime/package-info.java b/spring-context/src/main/java/org/springframework/format/datetime/package-info.java index b6f4686c34c1..a56e0229c418 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/package-info.java @@ -1,9 +1,7 @@ /** * Formatters for {@code java.util.Date} properties. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format.datetime; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java index 9e2f2668ec95..7ef08ebee3fe 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContext.java @@ -21,10 +21,11 @@ import java.time.format.DateTimeFormatter; import java.util.TimeZone; +import org.jspecify.annotations.Nullable; + import org.springframework.context.i18n.LocaleContext; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.context.i18n.TimeZoneAwareLocaleContext; -import org.springframework.lang.Nullable; /** * A context that holds user-specific java.time (JSR-310) settings @@ -37,11 +38,9 @@ */ public class DateTimeContext { - @Nullable - private Chronology chronology; + private @Nullable Chronology chronology; - @Nullable - private ZoneId timeZone; + private @Nullable ZoneId timeZone; /** @@ -54,8 +53,7 @@ public void setChronology(@Nullable Chronology chronology) { /** * Return the user's chronology (calendar system), if any. */ - @Nullable - public Chronology getChronology() { + public @Nullable Chronology getChronology() { return this.chronology; } @@ -74,8 +72,7 @@ public void setTimeZone(@Nullable ZoneId timeZone) { /** * Return the user's time zone, if any. */ - @Nullable - public ZoneId getTimeZone() { + public @Nullable ZoneId getTimeZone() { return this.timeZone; } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java index aa5a5223ef30..6fd18beef0a6 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeContextHolder.java @@ -19,8 +19,9 @@ import java.time.format.DateTimeFormatter; import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; /** * A holder for a thread-local user {@link DateTimeContext}. @@ -64,8 +65,7 @@ public static void setDateTimeContext(@Nullable DateTimeContext dateTimeContext) * Return the DateTimeContext associated with the current thread, if any. * @return the current DateTimeContext, or {@code null} if none */ - @Nullable - public static DateTimeContext getDateTimeContext() { + public static @Nullable DateTimeContext getDateTimeContext() { return dateTimeContextHolder.get(); } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java index f58b7d602101..8613ac179028 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactory.java @@ -20,8 +20,9 @@ import java.time.format.FormatStyle; import java.util.TimeZone; +import org.jspecify.annotations.Nullable; + import org.springframework.format.annotation.DateTimeFormat.ISO; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,20 +47,15 @@ */ public class DateTimeFormatterFactory { - @Nullable - private String pattern; + private @Nullable String pattern; - @Nullable - private ISO iso; + private @Nullable ISO iso; - @Nullable - private FormatStyle dateStyle; + private @Nullable FormatStyle dateStyle; - @Nullable - private FormatStyle timeStyle; + private @Nullable FormatStyle timeStyle; - @Nullable - private TimeZone timeZone; + private @Nullable TimeZone timeZone; /** @@ -137,8 +133,7 @@ public void setStylePattern(String style) { this.timeStyle = convertStyleCharacter(style.charAt(1)); } - @Nullable - private FormatStyle convertStyleCharacter(char c) { + private @Nullable FormatStyle convertStyleCharacter(char c) { return switch (c) { case 'S' -> FormatStyle.SHORT; case 'M' -> FormatStyle.MEDIUM; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java index 1a48b8c52ba3..35282f468011 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterFactoryBean.java @@ -18,9 +18,10 @@ import java.time.format.DateTimeFormatter; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} that creates a JSR-310 {@link java.time.format.DateTimeFormatter}. @@ -37,8 +38,7 @@ public class DateTimeFormatterFactoryBean extends DateTimeFormatterFactory implements FactoryBean, InitializingBean { - @Nullable - private DateTimeFormatter dateTimeFormatter; + private @Nullable DateTimeFormatter dateTimeFormatter; @Override @@ -47,8 +47,7 @@ public void afterPropertiesSet() { } @Override - @Nullable - public DateTimeFormatter getObject() { + public @Nullable DateTimeFormatter getObject() { return this.dateTimeFormatter; } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java index 8a5d09cf6bec..45adcc13d273 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DateTimeFormatterRegistrar.java @@ -51,7 +51,7 @@ * @see org.springframework.format.FormatterRegistrar#registerFormatters * @see org.springframework.format.datetime.DateFormatterRegistrar */ -@SuppressWarnings("NullAway") +@SuppressWarnings("NullAway") // Well-known map keys public class DateTimeFormatterRegistrar implements FormatterRegistrar { private enum Type {DATE, TIME, DATE_TIME} diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java index ee9a53a36cb0..cc920c7d7d16 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatter.java @@ -20,9 +20,10 @@ import java.time.Duration; import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.format.Formatter; import org.springframework.format.annotation.DurationFormat; -import org.springframework.lang.Nullable; /** * {@link Formatter} implementation for a JSR-310 {@link Duration}, @@ -37,8 +38,8 @@ public class DurationFormatter implements Formatter { private final DurationFormat.Style style; - @Nullable - private final DurationFormat.Unit defaultUnit; + + private final DurationFormat.@Nullable Unit defaultUnit; /** * Create a {@code DurationFormatter} following JSR-310's parsing rules for a Duration @@ -69,7 +70,7 @@ public DurationFormatter(DurationFormat.Style style) { * @param style the {@code DurationStyle} to use * @param defaultUnit the {@code DurationFormat.Unit} to fall back to when parsing and printing */ - public DurationFormatter(DurationFormat.Style style, @Nullable DurationFormat.Unit defaultUnit) { + public DurationFormatter(DurationFormat.Style style, DurationFormat.@Nullable Unit defaultUnit) { this.style = style; this.defaultUnit = defaultUnit; } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java index b1f5f3e58bd4..8e7c502ba470 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/DurationFormatterUtils.java @@ -20,8 +20,9 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + import org.springframework.format.annotation.DurationFormat; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -61,7 +62,7 @@ public static Duration parse(String value, DurationFormat.Style style) { * will default to ms) * @return a duration */ - public static Duration parse(String value, DurationFormat.Style style, @Nullable DurationFormat.Unit unit) { + public static Duration parse(String value, DurationFormat.Style style, DurationFormat.@Nullable Unit unit) { Assert.hasText(value, () -> "Value must not be empty"); return switch (style) { case ISO8601 -> parseIso8601(value); @@ -88,7 +89,7 @@ public static String print(Duration value, DurationFormat.Style style) { * to ms) * @return the printed result */ - public static String print(Duration value, DurationFormat.Style style, @Nullable DurationFormat.Unit unit) { + public static String print(Duration value, DurationFormat.Style style, DurationFormat.@Nullable Unit unit) { return switch (style) { case ISO8601 -> value.toString(); case SIMPLE -> printSimple(value, unit); @@ -116,7 +117,7 @@ public static Duration detectAndParse(String value) { * @throws IllegalArgumentException if the value is not a known style or cannot be * parsed */ - public static Duration detectAndParse(String value, @Nullable DurationFormat.Unit unit) { + public static Duration detectAndParse(String value, DurationFormat.@Nullable Unit unit) { return parse(value, detect(value), unit); } @@ -143,8 +144,8 @@ public static DurationFormat.Style detect(String value) { private static final Pattern ISO_8601_PATTERN = Pattern.compile("^[+-]?[pP].*$"); private static final Pattern SIMPLE_PATTERN = Pattern.compile("^([+-]?\\d+)([a-zA-Z]{0,2})$"); - private static final Pattern COMPOSITE_PATTERN = Pattern.compile("^([+-]?)\\(?\\s?(\\d+d)?\\s?(\\d+h)?\\s?(\\d+m)?" - + "\\s?(\\d+s)?\\s?(\\d+ms)?\\s?(\\d+us)?\\s?(\\d+ns)?\\)?$"); + private static final Pattern COMPOSITE_PATTERN = Pattern.compile("^([+-]?)\\(?\\s?(\\d+d)?\\s?(\\d+h)?\\s?(\\d+m)?" + + "\\s?(\\d+s)?\\s?(\\d+ms)?\\s?(\\d+us)?\\s?(\\d+ns)?\\)?$"); private static Duration parseIso8601(String value) { try { @@ -155,7 +156,7 @@ private static Duration parseIso8601(String value) { } } - private static Duration parseSimple(String text, @Nullable DurationFormat.Unit fallbackUnit) { + private static Duration parseSimple(String text, DurationFormat.@Nullable Unit fallbackUnit) { try { Matcher matcher = SIMPLE_PATTERN.matcher(text); Assert.state(matcher.matches(), "Does not match simple duration pattern"); @@ -171,7 +172,7 @@ private static Duration parseSimple(String text, @Nullable DurationFormat.Unit f } } - private static String printSimple(Duration duration, @Nullable DurationFormat.Unit unit) { + private static String printSimple(Duration duration, DurationFormat.@Nullable Unit unit) { unit = (unit == null ? DurationFormat.Unit.MILLIS : unit); return unit.print(duration); } diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java index eec58f9eac3c..0bac06c9656a 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/TemporalAccessorParser.java @@ -31,8 +31,9 @@ import java.time.temporal.TemporalAccessor; import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.format.Parser; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -59,11 +60,9 @@ public final class TemporalAccessorParser implements Parser { private final DateTimeFormatter formatter; - @Nullable - private final String[] fallbackPatterns; + private final String @Nullable [] fallbackPatterns; - @Nullable - private final Object source; + private final @Nullable Object source; /** @@ -77,7 +76,7 @@ public TemporalAccessorParser(Class temporalAccessor } TemporalAccessorParser(Class temporalAccessorType, DateTimeFormatter formatter, - @Nullable String[] fallbackPatterns, @Nullable Object source) { + String @Nullable [] fallbackPatterns, @Nullable Object source) { this.temporalAccessorType = temporalAccessorType; this.formatter = formatter; diff --git a/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java b/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java index fd73fe6cb3e7..ddd754486773 100644 --- a/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/datetime/standard/package-info.java @@ -1,9 +1,7 @@ /** * Integration with the JSR-310 java.time package in JDK 8. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format.datetime.standard; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java b/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java index 6c030d07868f..94f37a4458a4 100644 --- a/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/number/CurrencyStyleFormatter.java @@ -24,7 +24,7 @@ import java.util.Currency; import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A BigDecimal formatter for number values in currency style. @@ -43,14 +43,11 @@ public class CurrencyStyleFormatter extends AbstractNumberFormatter { private int fractionDigits = 2; - @Nullable - private RoundingMode roundingMode; + private @Nullable RoundingMode roundingMode; - @Nullable - private Currency currency; + private @Nullable Currency currency; - @Nullable - private String pattern; + private @Nullable String pattern; /** diff --git a/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java b/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java index 1597180306a6..8bc858a2d9ce 100644 --- a/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/number/NumberStyleFormatter.java @@ -20,7 +20,7 @@ import java.text.NumberFormat; import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A general-purpose number formatter using NumberFormat's number style. @@ -38,8 +38,7 @@ */ public class NumberStyleFormatter extends AbstractNumberFormatter { - @Nullable - private String pattern; + private @Nullable String pattern; /** diff --git a/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java b/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java index 1127b9299ccf..5e51eaefd3ab 100644 --- a/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java +++ b/spring-context/src/main/java/org/springframework/format/number/money/MonetaryAmountFormatter.java @@ -22,8 +22,9 @@ import javax.money.format.MonetaryAmountFormat; import javax.money.format.MonetaryFormats; +import org.jspecify.annotations.Nullable; + import org.springframework.format.Formatter; -import org.springframework.lang.Nullable; /** * Formatter for JSR-354 {@link javax.money.MonetaryAmount} values, @@ -36,8 +37,7 @@ */ public class MonetaryAmountFormatter implements Formatter { - @Nullable - private String formatName; + private @Nullable String formatName; /** diff --git a/spring-context/src/main/java/org/springframework/format/number/money/package-info.java b/spring-context/src/main/java/org/springframework/format/number/money/package-info.java index 91fbcd0f4cf3..79e044d413cf 100644 --- a/spring-context/src/main/java/org/springframework/format/number/money/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/number/money/package-info.java @@ -1,9 +1,7 @@ /** * Integration with the JSR-354 javax.money package. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format.number.money; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/number/package-info.java b/spring-context/src/main/java/org/springframework/format/number/package-info.java index 7fffd8adbb7f..6c3fb15ecbcb 100644 --- a/spring-context/src/main/java/org/springframework/format/number/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/number/package-info.java @@ -1,9 +1,7 @@ /** * Formatters for {@code java.lang.Number} properties. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format.number; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/package-info.java b/spring-context/src/main/java/org/springframework/format/package-info.java index 727b1ad0a9cf..a8e517b5a388 100644 --- a/spring-context/src/main/java/org/springframework/format/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/package-info.java @@ -1,9 +1,7 @@ /** * An API for defining Formatters to format field model values for display in a UI. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java index b3bd23631f52..50667bd8c7cb 100644 --- a/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/DefaultFormattingConversionService.java @@ -16,6 +16,8 @@ package org.springframework.format.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.format.FormatterRegistry; import org.springframework.format.datetime.DateFormatterRegistrar; @@ -24,7 +26,6 @@ import org.springframework.format.number.money.CurrencyUnitFormatter; import org.springframework.format.number.money.Jsr354NumberFormatAnnotationFormatterFactory; import org.springframework.format.number.money.MonetaryAmountFormatter; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringValueResolver; diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java index 54ec2a76e9aa..11de9d064774 100644 --- a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionService.java @@ -22,6 +22,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.context.EmbeddedValueResolverAware; import org.springframework.context.i18n.LocaleContextHolder; import org.springframework.core.DecoratingProxy; @@ -36,7 +38,6 @@ import org.springframework.format.FormatterRegistry; import org.springframework.format.Parser; import org.springframework.format.Printer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -53,8 +54,7 @@ public class FormattingConversionService extends GenericConversionService implements FormatterRegistry, EmbeddedValueResolverAware { - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; private final Map cachedPrinters = new ConcurrentHashMap<>(64); @@ -174,8 +174,7 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe return this.printer.print(source, LocaleContextHolder.getLocale()); } - @Nullable - private Class resolvePrinterObjectType(Printer printer) { + private @Nullable Class resolvePrinterObjectType(Printer printer) { return GenericTypeResolver.resolveTypeArgument(printer.getClass(), Printer.class); } @@ -206,8 +205,7 @@ public Set getConvertibleTypes() { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { String text = (String) source; if (!StringUtils.hasText(text)) { return null; @@ -265,8 +263,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { @Override @SuppressWarnings("unchecked") - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { Annotation ann = sourceType.getAnnotation(this.annotationType); if (ann == null) { throw new IllegalStateException( @@ -320,8 +317,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { @Override @SuppressWarnings("unchecked") - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { Annotation ann = targetType.getAnnotation(this.annotationType); if (ann == null) { throw new IllegalStateException( diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java index c4e0ca1a2787..075250cbf548 100644 --- a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceFactoryBean.java @@ -18,6 +18,8 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.EmbeddedValueResolverAware; @@ -28,7 +30,6 @@ import org.springframework.format.FormatterRegistry; import org.springframework.format.Parser; import org.springframework.format.Printer; -import org.springframework.lang.Nullable; import org.springframework.util.StringValueResolver; /** @@ -59,22 +60,17 @@ public class FormattingConversionServiceFactoryBean implements FactoryBean, EmbeddedValueResolverAware, InitializingBean { - @Nullable - private Set converters; + private @Nullable Set converters; - @Nullable - private Set formatters; + private @Nullable Set formatters; - @Nullable - private Set formatterRegistrars; + private @Nullable Set formatterRegistrars; private boolean registerDefaultFormatters = true; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; - @Nullable - private FormattingConversionService conversionService; + private @Nullable FormattingConversionService conversionService; /** @@ -162,8 +158,7 @@ else if (candidate instanceof AnnotationFormatterFactory factory) { @Override - @Nullable - public FormattingConversionService getObject() { + public @Nullable FormattingConversionService getObject() { return this.conversionService; } diff --git a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java index d1f41d84fcb3..a0aebe73f34d 100644 --- a/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java +++ b/spring-context/src/main/java/org/springframework/format/support/FormattingConversionServiceRuntimeHints.java @@ -16,10 +16,11 @@ package org.springframework.format.support; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} to register hints for {@link DefaultFormattingConversionService}. diff --git a/spring-context/src/main/java/org/springframework/format/support/package-info.java b/spring-context/src/main/java/org/springframework/format/support/package-info.java index 27db8d50e5f4..0e6e1a4723a3 100644 --- a/spring-context/src/main/java/org/springframework/format/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/format/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for the formatting package, * providing common implementations as well as adapters. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.format.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java index b3b57cab0554..7fda4a4d42d0 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/InstrumentationLoadTimeWeaver.java @@ -23,8 +23,9 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.instrument.InstrumentationSavingAgent; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -56,11 +57,9 @@ public class InstrumentationLoadTimeWeaver implements LoadTimeWeaver { InstrumentationLoadTimeWeaver.class.getClassLoader()); - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; - @Nullable - private final Instrumentation instrumentation; + private final @Nullable Instrumentation instrumentation; private final List transformers = new ArrayList<>(4); @@ -142,8 +141,7 @@ public static boolean isInstrumentationAvailable() { * @return the Instrumentation instance, or {@code null} if none found * @see #isInstrumentationAvailable() */ - @Nullable - private static Instrumentation getInstrumentation() { + private static @Nullable Instrumentation getInstrumentation() { if (AGENT_CLASS_PRESENT) { return InstrumentationAccessor.getInstrumentation(); } @@ -171,8 +169,7 @@ private static class FilteringClassFileTransformer implements ClassFileTransform private final ClassFileTransformer targetTransformer; - @Nullable - private final ClassLoader targetClassLoader; + private final @Nullable ClassLoader targetClassLoader; public FilteringClassFileTransformer( ClassFileTransformer targetTransformer, @Nullable ClassLoader targetClassLoader) { @@ -182,8 +179,7 @@ public FilteringClassFileTransformer( } @Override - @Nullable - public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, + public byte @Nullable [] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { if (this.targetClassLoader != loader) { diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java index ba6fe6084250..ccbd18233c27 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ReflectiveLoadTimeWeaver.java @@ -21,10 +21,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.DecoratingClassLoader; import org.springframework.core.OverridingClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -73,8 +73,7 @@ public class ReflectiveLoadTimeWeaver implements LoadTimeWeaver { private final Method addTransformerMethod; - @Nullable - private final Method getThrowawayClassLoaderMethod; + private final @Nullable Method getThrowawayClassLoaderMethod; /** diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java index 21b75d9c2f2b..fcd07532e3d6 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ResourceOverridingShadowingClassLoader.java @@ -23,7 +23,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -105,8 +106,7 @@ public URL getResource(String requestedPath) { } @Override - @Nullable - public InputStream getResourceAsStream(String requestedPath) { + public @Nullable InputStream getResourceAsStream(String requestedPath) { if (this.overrides.containsKey(requestedPath)) { String overriddenPath = this.overrides.get(requestedPath); return (overriddenPath != null ? super.getResourceAsStream(overriddenPath) : null); diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java index 0512d51de349..448be7e92bc4 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/ShadowingClassLoader.java @@ -27,8 +27,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.DecoratingClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.util.StringUtils; @@ -191,8 +192,7 @@ public URL getResource(String name) { } @Override - @Nullable - public InputStream getResourceAsStream(String name) { + public @Nullable InputStream getResourceAsStream(String name) { return this.enclosingClassLoader.getResourceAsStream(name); } diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java index b61cceb02ec2..c4debbcaedf2 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleInstrumentableClassLoader.java @@ -18,8 +18,9 @@ import java.lang.instrument.ClassFileTransformer; +import org.jspecify.annotations.Nullable; + import org.springframework.core.OverridingClassLoader; -import org.springframework.lang.Nullable; /** * Simplistic implementation of an instrumentable {@code ClassLoader}. diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java index a4329ae2a2c5..44f4ebeba4fc 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/SimpleThrowawayClassLoader.java @@ -16,8 +16,9 @@ package org.springframework.instrument.classloading; +import org.jspecify.annotations.Nullable; + import org.springframework.core.OverridingClassLoader; -import org.springframework.lang.Nullable; /** * ClassLoader that can be used to load classes without bringing them diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java index 7f9a06d52d78..8188b5a71eb5 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/WeavingTransformer.java @@ -22,7 +22,8 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -40,8 +41,7 @@ */ public class WeavingTransformer { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final List transformers = new ArrayList<>(); diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java index 6df3504ca9c3..a1ddc170dc70 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/GlassFishLoadTimeWeaver.java @@ -20,9 +20,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.core.OverridingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java index 7ab813fa0a9f..b282705390bf 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/glassfish/package-info.java @@ -1,9 +1,4 @@ /** * Support for class instrumentation on GlassFish. */ -@NonNullApi -@NonNullFields package org.springframework.instrument.classloading.glassfish; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java index f6db661d974e..7f98672b3083 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/JBossLoadTimeWeaver.java @@ -21,9 +21,10 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java index 74561954733d..e746ce5e7f5b 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/jboss/package-info.java @@ -1,9 +1,4 @@ /** * Support for class instrumentation on JBoss AS 6 and 7. */ -@NonNullApi -@NonNullFields package org.springframework.instrument.classloading.jboss; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java index 82ed42c3b699..c1b7cf3d2cbd 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/package-info.java @@ -2,9 +2,4 @@ * Support package for load time weaving based on class loaders, * as required by JPA providers (but not JPA-specific). */ -@NonNullApi -@NonNullFields package org.springframework.instrument.classloading; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java index a22203404e76..96032cfe54b4 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/TomcatLoadTimeWeaver.java @@ -20,9 +20,10 @@ import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.core.OverridingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java index c1ac847dad67..11c70a1c6c5d 100644 --- a/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java +++ b/spring-context/src/main/java/org/springframework/instrument/classloading/tomcat/package-info.java @@ -1,9 +1,4 @@ /** * Support for class instrumentation on Tomcat. */ -@NonNullApi -@NonNullFields package org.springframework.instrument.classloading.tomcat; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java b/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java index a6e8a89c823a..c51a9562684e 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/ConnectorDelegate.java @@ -26,10 +26,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jmx.MBeanServerNotFoundException; import org.springframework.jmx.support.JmxUtils; -import org.springframework.lang.Nullable; /** * Internal helper class for managing a JMX connector. @@ -41,8 +41,7 @@ class ConnectorDelegate { private static final Log logger = LogFactory.getLog(ConnectorDelegate.class); - @Nullable - private JMXConnector connector; + private @Nullable JMXConnector connector; /** diff --git a/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java b/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java index 2cd42366d217..55494b1ce5d4 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/InvalidInvocationException.java @@ -18,7 +18,7 @@ import javax.management.JMRuntimeException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown when trying to invoke an operation on a proxy that is not exposed diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java index 063993eb0983..4e471f82e6c7 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanClientInterceptor.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,6 +53,7 @@ import org.aopalliance.intercept.MethodInvocation; 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.BeanClassLoaderAware; @@ -63,7 +64,6 @@ import org.springframework.core.ResolvableType; import org.springframework.jmx.support.JmxUtils; import org.springframework.jmx.support.ObjectNameManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -97,40 +97,31 @@ public class MBeanClientInterceptor /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private MBeanServerConnection server; + private @Nullable MBeanServerConnection server; - @Nullable - private JMXServiceURL serviceUrl; + private @Nullable JMXServiceURL serviceUrl; - @Nullable - private Map environment; + private @Nullable Map environment; - @Nullable - private String agentId; + private @Nullable String agentId; private boolean connectOnStartup = true; private boolean refreshOnConnectFailure = false; - @Nullable - private ObjectName objectName; + private @Nullable ObjectName objectName; private boolean useStrictCasing = true; - @Nullable - private Class managementInterface; + private @Nullable Class managementInterface; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private final ConnectorDelegate connector = new ConnectorDelegate(); - @Nullable - private MBeanServerConnection serverToUse; + private @Nullable MBeanServerConnection serverToUse; - @Nullable - private MBeanServerInvocationHandler invocationHandler; + private @Nullable MBeanServerInvocationHandler invocationHandler; private Map allowedAttributes = Collections.emptyMap(); @@ -171,8 +162,7 @@ public void setEnvironment(@Nullable Map environment) { * {@code environment[myKey]}. This is particularly useful for * adding or overriding entries in child bean definitions. */ - @Nullable - public Map getEnvironment() { + public @Nullable Map getEnvironment() { return this.environment; } @@ -239,8 +229,7 @@ public void setManagementInterface(@Nullable Class managementInterface) { * Return the management interface of the target MBean, * or {@code null} if none specified. */ - @Nullable - protected final Class getManagementInterface() { + protected final @Nullable Class getManagementInterface() { return this.managementInterface; } @@ -356,8 +345,7 @@ protected boolean isPrepared() { * @see #handleConnectFailure */ @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { // Lazily connect to MBeanServer if necessary. synchronized (this.preparationMonitor) { if (!isPrepared()) { @@ -384,8 +372,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { * @see #setRefreshOnConnectFailure * @see #doInvoke */ - @Nullable - protected Object handleConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { + protected @Nullable Object handleConnectFailure(MethodInvocation invocation, Exception ex) throws Throwable { if (this.refreshOnConnectFailure) { String msg = "Could not connect to JMX server - retrying"; if (logger.isDebugEnabled()) { @@ -410,8 +397,7 @@ else if (logger.isWarnEnabled()) { * @return the value returned as a result of the re-routed invocation * @throws Throwable an invocation error propagated to the user */ - @Nullable - protected Object doInvoke(MethodInvocation invocation) throws Throwable { + protected @Nullable Object doInvoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); try { Object result; @@ -477,8 +463,7 @@ else if (rex instanceof RuntimeErrorException runtimeErrorException) { } } - @Nullable - private Object invokeAttribute(PropertyDescriptor pd, MethodInvocation invocation) + private @Nullable Object invokeAttribute(PropertyDescriptor pd, MethodInvocation invocation) throws JMException, IOException { Assert.state(this.serverToUse != null, "No MBeanServerConnection available"); @@ -522,7 +507,7 @@ else if (invocation.getMethod().equals(pd.getWriteMethod())) { * @param args the invocation arguments * @return the value returned by the method invocation. */ - private Object invokeOperation(Method method, Object[] args) throws JMException, IOException { + private Object invokeOperation(Method method, @Nullable Object[] args) throws JMException, IOException { Assert.state(this.serverToUse != null, "No MBeanServerConnection available"); MethodCacheKey key = new MethodCacheKey(method.getName(), method.getParameterTypes()); @@ -552,8 +537,7 @@ private Object invokeOperation(Method method, Object[] args) throws JMException, * @return the converted result object, or the passed-in object if no conversion * is necessary */ - @Nullable - protected Object convertResultValueIfNecessary(@Nullable Object result, MethodParameter parameter) { + protected @Nullable Object convertResultValueIfNecessary(@Nullable Object result, MethodParameter parameter) { Class targetClass = parameter.getParameterType(); try { if (result == null) { @@ -648,7 +632,7 @@ private static final class MethodCacheKey implements Comparable * @param name the name of the method * @param parameterTypes the arguments in the method signature */ - public MethodCacheKey(String name, @Nullable Class[] parameterTypes) { + public MethodCacheKey(String name, Class @Nullable [] parameterTypes) { this.name = name; this.parameterTypes = (parameterTypes != null ? parameterTypes : new Class[0]); } diff --git a/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java index b0c62aa13a17..77e671667ee7 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/MBeanProxyFactoryBean.java @@ -16,12 +16,13 @@ package org.springframework.jmx.access; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.jmx.MBeanServerNotFoundException; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -49,14 +50,11 @@ public class MBeanProxyFactoryBean extends MBeanClientInterceptor implements FactoryBean, BeanClassLoaderAware, InitializingBean { - @Nullable - private Class proxyInterface; + private @Nullable Class proxyInterface; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private Object mbeanProxy; + private @Nullable Object mbeanProxy; /** @@ -102,14 +100,12 @@ public void afterPropertiesSet() throws MBeanServerNotFoundException, MBeanInfoR @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.mbeanProxy; } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { return this.proxyInterface; } diff --git a/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java b/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java index 159e067e6126..c2f18b3ee8fe 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/NotificationListenerRegistrar.java @@ -27,13 +27,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.jmx.JmxException; import org.springframework.jmx.MBeanServerNotFoundException; import org.springframework.jmx.support.NotificationListenerHolder; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -55,20 +55,15 @@ public class NotificationListenerRegistrar extends NotificationListenerHolder private final ConnectorDelegate connector = new ConnectorDelegate(); - @Nullable - private MBeanServerConnection server; + private @Nullable MBeanServerConnection server; - @Nullable - private JMXServiceURL serviceUrl; + private @Nullable JMXServiceURL serviceUrl; - @Nullable - private Map environment; + private @Nullable Map environment; - @Nullable - private String agentId; + private @Nullable String agentId; - @Nullable - private ObjectName[] actualObjectNames; + private ObjectName @Nullable [] actualObjectNames; /** @@ -94,8 +89,7 @@ public void setEnvironment(@Nullable Map environment) { * {@code environment[myKey]}. This is particularly useful for * adding or overriding entries in child bean definitions. */ - @Nullable - public Map getEnvironment() { + public @Nullable Map getEnvironment() { return this.environment; } diff --git a/spring-context/src/main/java/org/springframework/jmx/access/package-info.java b/spring-context/src/main/java/org/springframework/jmx/access/package-info.java index adac687a46f2..70e314fd6579 100644 --- a/spring-context/src/main/java/org/springframework/jmx/access/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/access/package-info.java @@ -1,9 +1,7 @@ /** * Provides support for accessing remote MBean resources. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.access; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 ec845f54d85c..af524d54d61d 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. @@ -38,6 +38,8 @@ import javax.management.modelmbean.ModelMBeanInfo; import javax.management.modelmbean.RequiredModelMBean; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.aop.support.AopUtils; @@ -62,7 +64,6 @@ import org.springframework.jmx.export.notification.NotificationPublisherAware; import org.springframework.jmx.support.JmxUtils; import org.springframework.jmx.support.MBeanRegistrationSupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -154,12 +155,10 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo /** The beans to be exposed as JMX managed resources, with JMX names as keys. */ - @Nullable - private Map beans; + private @Nullable Map beans; /** The autodetect mode to use for this MBeanExporter. */ - @Nullable - Integer autodetectMode; + @Nullable Integer autodetectMode; /** Whether to eagerly initialize candidate beans when auto-detecting MBeans. */ private boolean allowEagerInit = false; @@ -180,23 +179,19 @@ public class MBeanExporter extends MBeanRegistrationSupport implements MBeanExpo private final Set excludedBeans = new HashSet<>(); /** The MBeanExporterListeners registered with this exporter. */ - @Nullable - private MBeanExporterListener[] listeners; + private MBeanExporterListener @Nullable [] listeners; /** The NotificationListeners to register for the MBeans registered by this exporter. */ - @Nullable - private NotificationListenerBean[] notificationListeners; + private NotificationListenerBean @Nullable [] notificationListeners; /** Map of actually registered NotificationListeners. */ private final Map registeredNotificationListeners = new LinkedHashMap<>(); /** Stores the ClassLoader to use for generating lazy-init proxies. */ - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** Stores the BeanFactory for use in auto-detection process. */ - @Nullable - private ListableBeanFactory beanFactory; + private @Nullable ListableBeanFactory beanFactory; /** @@ -794,8 +789,7 @@ protected boolean isMBean(@Nullable Class beanClass) { * @return the adapted MBean, or {@code null} if not possible */ @SuppressWarnings("unchecked") - @Nullable - protected DynamicMBean adaptMBeanIfPossible(Object bean) throws JMException { + protected @Nullable DynamicMBean adaptMBeanIfPossible(Object bean) throws JMException { Class targetClass = AopUtils.getTargetClass(bean); if (targetClass != bean.getClass()) { Class ifc = JmxUtils.getMXBeanInterface(targetClass); @@ -932,8 +926,8 @@ private void autodetect(Map beans, AutodetectCallback callback) */ 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 '&' } /** @@ -993,7 +987,6 @@ private void registerNotificationListeners() throws MBeanExportException { * Unregister the configured {@link NotificationListener NotificationListeners} * from the {@link MBeanServer}. */ - @SuppressWarnings("NullAway") private void unregisterNotificationListeners() { if (this.server != null) { this.registeredNotificationListeners.forEach((bean, mappedObjectNames) -> { @@ -1097,11 +1090,9 @@ private interface AutodetectCallback { @SuppressWarnings("serial") private class NotificationPublisherAwareLazyTargetSource extends LazyInitTargetSource { - @Nullable - private ModelMBean modelMBean; + private @Nullable ModelMBean modelMBean; - @Nullable - private ObjectName objectName; + private @Nullable ObjectName objectName; public void setModelMBean(ModelMBean modelMBean) { this.modelMBean = modelMBean; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java index 3398cc8803cd..076b652fb9a7 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/AnnotationJmxAttributeSource.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,6 +26,8 @@ import java.util.Map; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.MutablePropertyValues; @@ -39,10 +41,8 @@ import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.core.annotation.RepeatableContainers; import org.springframework.jmx.export.metadata.InvalidMetadataException; import org.springframework.jmx.export.metadata.JmxAttributeSource; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.util.StringValueResolver; @@ -61,8 +61,7 @@ */ public class AnnotationJmxAttributeSource implements JmxAttributeSource, BeanFactoryAware { - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; @Override @@ -74,8 +73,9 @@ public void setBeanFactory(BeanFactory beanFactory) { @Override - @Nullable - public org.springframework.jmx.export.metadata.ManagedResource getManagedResource(Class beanClass) throws InvalidMetadataException { + public org.springframework.jmx.export.metadata.@Nullable ManagedResource getManagedResource(Class beanClass) + throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(beanClass, SearchStrategy.TYPE_HIERARCHY) .get(ManagedResource.class).withNonMergedAttributes(); if (!ann.isPresent()) { @@ -87,7 +87,8 @@ public org.springframework.jmx.export.metadata.ManagedResource getManagedResourc throw new InvalidMetadataException("@ManagedResource class '" + target.getName() + "' must be public"); } - org.springframework.jmx.export.metadata.ManagedResource bean = new org.springframework.jmx.export.metadata.ManagedResource(); + org.springframework.jmx.export.metadata.ManagedResource bean = + new org.springframework.jmx.export.metadata.ManagedResource(); Map map = ann.asMap(); List list = new ArrayList<>(map.size()); map.forEach((attrName, attrValue) -> { @@ -104,15 +105,17 @@ public org.springframework.jmx.export.metadata.ManagedResource getManagedResourc } @Override - @Nullable - public org.springframework.jmx.export.metadata.ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException { + public org.springframework.jmx.export.metadata.@Nullable ManagedAttribute getManagedAttribute(Method method) + throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) .get(ManagedAttribute.class).withNonMergedAttributes(); if (!ann.isPresent()) { return null; } - org.springframework.jmx.export.metadata.ManagedAttribute bean = new org.springframework.jmx.export.metadata.ManagedAttribute(); + org.springframework.jmx.export.metadata.ManagedAttribute bean = + new org.springframework.jmx.export.metadata.ManagedAttribute(); Map map = ann.asMap(); MutablePropertyValues pvs = new MutablePropertyValues(map); pvs.removePropertyValue("defaultValue"); @@ -125,8 +128,9 @@ public org.springframework.jmx.export.metadata.ManagedAttribute getManagedAttrib } @Override - @Nullable - public org.springframework.jmx.export.metadata.ManagedMetric getManagedMetric(Method method) throws InvalidMetadataException { + public org.springframework.jmx.export.metadata.@Nullable ManagedMetric getManagedMetric(Method method) + throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) .get(ManagedMetric.class).withNonMergedAttributes(); @@ -134,8 +138,9 @@ public org.springframework.jmx.export.metadata.ManagedMetric getManagedMetric(Me } @Override - @Nullable - public org.springframework.jmx.export.metadata.ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException { + public org.springframework.jmx.export.metadata.@Nullable ManagedOperation getManagedOperation(Method method) + throws InvalidMetadataException { + MergedAnnotation ann = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY) .get(ManagedOperation.class).withNonMergedAttributes(); @@ -143,32 +148,26 @@ public org.springframework.jmx.export.metadata.ManagedOperation getManagedOperat } @Override - public org.springframework.jmx.export.metadata.ManagedOperationParameter[] getManagedOperationParameters(Method method) - throws InvalidMetadataException { - - List> anns = getRepeatableAnnotations( - method, ManagedOperationParameter.class, ManagedOperationParameters.class); + public org.springframework.jmx.export.metadata.@Nullable ManagedOperationParameter[] getManagedOperationParameters( + Method method) throws InvalidMetadataException { + List> anns = getRepeatableAnnotations(method, ManagedOperationParameter.class); return copyPropertiesToBeanArray(anns, org.springframework.jmx.export.metadata.ManagedOperationParameter.class); } @Override - public org.springframework.jmx.export.metadata.ManagedNotification[] getManagedNotifications(Class clazz) + public org.springframework.jmx.export.metadata.@Nullable ManagedNotification[] getManagedNotifications(Class clazz) throws InvalidMetadataException { - List> anns = getRepeatableAnnotations( - clazz, ManagedNotification.class, ManagedNotifications.class); - + List> anns = getRepeatableAnnotations(clazz, ManagedNotification.class); return copyPropertiesToBeanArray(anns, org.springframework.jmx.export.metadata.ManagedNotification.class); } private static List> getRepeatableAnnotations( - AnnotatedElement annotatedElement, Class annotationType, - Class containerAnnotationType) { + AnnotatedElement annotatedElement, Class annotationType) { - return MergedAnnotations.from(annotatedElement, SearchStrategy.TYPE_HIERARCHY, - RepeatableContainers.of(annotationType, containerAnnotationType)) + return MergedAnnotations.from(annotatedElement, SearchStrategy.TYPE_HIERARCHY) .stream(annotationType) .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) .map(MergedAnnotation::withNonMergedAttributes) @@ -176,10 +175,10 @@ private static List> getRepeatableAnnotat } @SuppressWarnings("unchecked") - private static T[] copyPropertiesToBeanArray( + private static @Nullable T[] copyPropertiesToBeanArray( List> anns, Class beanClass) { - T[] beans = (T[]) Array.newInstance(beanClass, anns.size()); + @Nullable T[] beans = (T[]) Array.newInstance(beanClass, anns.size()); int i = 0; for (MergedAnnotation ann : anns) { beans[i++] = copyPropertiesToBean(ann, beanClass); @@ -187,8 +186,7 @@ private static T[] copyPropertiesToBeanArray( return beans; } - @Nullable - private static T copyPropertiesToBean(MergedAnnotation ann, Class beanClass) { + private static @Nullable T copyPropertiesToBean(MergedAnnotation ann, Class beanClass) { if (!ann.isPresent()) { return null; } 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 0ccf57a82053..89aa97786aa7 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 @@ @Documented public @interface ManagedOperationParameters { - ManagedOperationParameter[] value() default {}; + ManagedOperationParameter[] value(); } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java index ee176795f649..19719ff67272 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/annotation/package-info.java @@ -4,9 +4,7 @@ *

Hooked into Spring's JMX export infrastructure via a special * {@link org.springframework.jmx.export.metadata.JmxAttributeSource} implementation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.export.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java index dba27a1205f6..663164005e0f 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractConfigurableMBeanInfoAssembler.java @@ -24,9 +24,10 @@ import javax.management.modelmbean.ModelMBeanNotificationInfo; +import org.jspecify.annotations.Nullable; + import org.springframework.jmx.export.metadata.JmxMetadataUtils; import org.springframework.jmx.export.metadata.ManagedNotification; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -39,8 +40,7 @@ */ public abstract class AbstractConfigurableMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler { - @Nullable - private ModelMBeanNotificationInfo[] notificationInfos; + private ModelMBeanNotificationInfo @Nullable [] notificationInfos; private final Map notificationInfoMappings = new HashMap<>(); diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java index 11902dea7329..1f32cd256d62 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/AbstractReflectiveMBeanInfoAssembler.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. @@ -28,13 +28,14 @@ import javax.management.modelmbean.ModelMBeanAttributeInfo; import javax.management.modelmbean.ModelMBeanOperationInfo; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeanUtils; import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.jmx.support.JmxUtils; -import org.springframework.lang.Nullable; /** * Builds on the {@link AbstractMBeanInfoAssembler} superclass to @@ -173,8 +174,7 @@ public abstract class AbstractReflectiveMBeanInfoAssembler extends AbstractMBean /** * Default value for the JMX field "currencyTimeLimit". */ - @Nullable - private Integer defaultCurrencyTimeLimit; + private @Nullable Integer defaultCurrencyTimeLimit; /** * Indicates whether strict casing is being used for attributes. @@ -183,8 +183,7 @@ public abstract class AbstractReflectiveMBeanInfoAssembler extends AbstractMBean private boolean exposeClassDescriptor = false; - @Nullable - private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); + private @Nullable ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer(); /** @@ -214,8 +213,7 @@ public void setDefaultCurrencyTimeLimit(@Nullable Integer defaultCurrencyTimeLim /** * Return default value for the JMX field "currencyTimeLimit", if any. */ - @Nullable - protected Integer getDefaultCurrencyTimeLimit() { + protected @Nullable Integer getDefaultCurrencyTimeLimit() { return this.defaultCurrencyTimeLimit; } @@ -277,8 +275,7 @@ public void setParameterNameDiscoverer(@Nullable ParameterNameDiscoverer paramet * Return the ParameterNameDiscoverer to use for resolving method parameter * names if needed (may be {@code null} in order to skip parameter detection). */ - @Nullable - protected ParameterNameDiscoverer getParameterNameDiscoverer() { + protected @Nullable ParameterNameDiscoverer getParameterNameDiscoverer() { return this.parameterNameDiscoverer; } @@ -511,7 +508,7 @@ protected String getOperationDescription(Method method, String beanKey) { */ protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) { ParameterNameDiscoverer paramNameDiscoverer = getParameterNameDiscoverer(); - String[] paramNames = (paramNameDiscoverer != null ? paramNameDiscoverer.getParameterNames(method) : null); + @Nullable String[] paramNames = (paramNameDiscoverer != null ? paramNameDiscoverer.getParameterNames(method) : null); if (paramNames == null) { return new MBeanParameterInfo[0]; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java index 4be4b5e050d5..8a838f4a3f0f 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/InterfaceBasedMBeanInfoAssembler.java @@ -23,9 +23,10 @@ import java.util.Map; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -61,19 +62,15 @@ public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler implements BeanClassLoaderAware, InitializingBean { - @Nullable - private Class[] managedInterfaces; + private Class @Nullable [] managedInterfaces; /** Mappings of bean keys to an array of classes. */ - @Nullable - private Properties interfaceMappings; + private @Nullable Properties interfaceMappings; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); /** Mappings of bean keys to an array of classes. */ - @Nullable - private Map[]> resolvedInterfaceMappings; + private @Nullable Map[]> resolvedInterfaceMappings; /** @@ -84,7 +81,7 @@ public class InterfaceBasedMBeanInfoAssembler extends AbstractConfigurableMBeanI * Each entry MUST be an interface. * @see #setInterfaceMappings */ - public void setManagedInterfaces(@Nullable Class... managedInterfaces) { + public void setManagedInterfaces(Class @Nullable ... managedInterfaces) { if (managedInterfaces != null) { for (Class ifc : managedInterfaces) { if (!ifc.isInterface()) { diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java index 86d0cca923e2..712ea3138f01 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MetadataMBeanInfoAssembler.java @@ -18,11 +18,14 @@ import java.beans.PropertyDescriptor; import java.lang.reflect.Method; +import java.util.Objects; import javax.management.Descriptor; import javax.management.MBeanParameterInfo; import javax.management.modelmbean.ModelMBeanNotificationInfo; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.InitializingBean; @@ -35,7 +38,6 @@ import org.springframework.jmx.export.metadata.ManagedOperation; import org.springframework.jmx.export.metadata.ManagedOperationParameter; import org.springframework.jmx.export.metadata.ManagedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -59,8 +61,7 @@ public class MetadataMBeanInfoAssembler extends AbstractReflectiveMBeanInfoAssembler implements AutodetectCapableMBeanInfoAssembler, InitializingBean { - @Nullable - private JmxAttributeSource attributeSource; + private @Nullable JmxAttributeSource attributeSource; /** @@ -259,7 +260,7 @@ protected String getOperationDescription(Method method, String beanKey) { */ @Override protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) { - ManagedOperationParameter[] params = obtainAttributeSource().getManagedOperationParameters(method); + @Nullable ManagedOperationParameter[] params = obtainAttributeSource().getManagedOperationParameters(method); if (ObjectUtils.isEmpty(params)) { return super.getOperationParameters(method, beanKey); } @@ -267,7 +268,7 @@ protected MBeanParameterInfo[] getOperationParameters(Method method, String bean MBeanParameterInfo[] parameterInfo = new MBeanParameterInfo[params.length]; Class[] methodParameters = method.getParameterTypes(); for (int i = 0; i < params.length; i++) { - ManagedOperationParameter param = params[i]; + ManagedOperationParameter param = Objects.requireNonNull(params[i]); parameterInfo[i] = new MBeanParameterInfo(param.getName(), methodParameters[i].getName(), param.getDescription()); } @@ -280,14 +281,14 @@ protected MBeanParameterInfo[] getOperationParameters(Method method, String bean */ @Override protected ModelMBeanNotificationInfo[] getNotificationInfo(Object managedBean, String beanKey) { - ManagedNotification[] notificationAttributes = + @Nullable ManagedNotification[] notificationAttributes = obtainAttributeSource().getManagedNotifications(getClassToExpose(managedBean)); ModelMBeanNotificationInfo[] notificationInfos = new ModelMBeanNotificationInfo[notificationAttributes.length]; for (int i = 0; i < notificationAttributes.length; i++) { ManagedNotification attribute = notificationAttributes[i]; - notificationInfos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(attribute); + notificationInfos[i] = JmxMetadataUtils.convertToModelMBeanNotificationInfo(Objects.requireNonNull(attribute)); } return notificationInfos; @@ -428,8 +429,7 @@ private int resolveIntDescriptor(int getter, int setter) { * @param setter the Object value associated with the set method * @return the appropriate Object to use as the value for the descriptor */ - @Nullable - private Object resolveObjectDescriptor(@Nullable Object getter, @Nullable Object setter) { + private @Nullable Object resolveObjectDescriptor(@Nullable Object getter, @Nullable Object setter) { return (getter != null ? getter : setter); } @@ -443,8 +443,7 @@ private Object resolveObjectDescriptor(@Nullable Object getter, @Nullable Object * @param setter the String value associated with the set method * @return the appropriate String to use as the value for the descriptor */ - @Nullable - private String resolveStringDescriptor(@Nullable String getter, @Nullable String setter) { + private @Nullable String resolveStringDescriptor(@Nullable String getter, @Nullable String setter) { return (StringUtils.hasLength(getter) ? getter : setter); } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java index af780d54a86a..3e54718a5626 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodExclusionMBeanInfoAssembler.java @@ -23,7 +23,8 @@ import java.util.Properties; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -56,11 +57,9 @@ */ public class MethodExclusionMBeanInfoAssembler extends AbstractConfigurableMBeanInfoAssembler { - @Nullable - private Set ignoredMethods; + private @Nullable Set ignoredMethods; - @Nullable - private Map> ignoredMethodMappings; + private @Nullable Map> ignoredMethodMappings; /** diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java index f0d1392d5783..503564486792 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/MethodNameBasedMBeanInfoAssembler.java @@ -23,7 +23,8 @@ import java.util.Properties; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -56,14 +57,12 @@ public class MethodNameBasedMBeanInfoAssembler extends AbstractConfigurableMBean /** * Stores the set of method names to use for creating the management interface. */ - @Nullable - private Set managedMethods; + private @Nullable Set managedMethods; /** * Stores the mappings of bean keys to an array of method names. */ - @Nullable - private Map> methodMappings; + private @Nullable Map> methodMappings; /** diff --git a/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java index cb5997945d62..b144b30663a0 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/assembler/package-info.java @@ -2,9 +2,7 @@ * Provides a strategy for MBeanInfo assembly. Used by MBeanExporter to * determine the attributes and operations to expose for Spring-managed beans. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.export.assembler; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java index 3921d122505d..d281a39c409f 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/JmxAttributeSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface used by the {@code MetadataMBeanInfoAssembler} to @@ -33,69 +33,58 @@ public interface JmxAttributeSource { /** - * Implementations should return an instance of {@code ManagedResource} - * if the supplied {@code Class} has the appropriate metadata. - * Otherwise, should return {@code null}. - * @param clazz the class to read the attribute data from - * @return the attribute, or {@code null} if not found - * @throws InvalidMetadataException in case of invalid attributes + * Implementations should return an instance of {@link ManagedResource} + * if the supplied {@code Class} has the corresponding metadata. + * @param clazz the class to read the resource data from + * @return the resource, or {@code null} if not found + * @throws InvalidMetadataException in case of invalid metadata */ - @Nullable - ManagedResource getManagedResource(Class clazz) throws InvalidMetadataException; + @Nullable ManagedResource getManagedResource(Class clazz) throws InvalidMetadataException; /** - * Implementations should return an instance of {@code ManagedAttribute} + * Implementations should return an instance of {@link ManagedAttribute} * if the supplied {@code Method} has the corresponding metadata. - * Otherwise, should return {@code null}. * @param method the method to read the attribute data from * @return the attribute, or {@code null} if not found - * @throws InvalidMetadataException in case of invalid attributes + * @throws InvalidMetadataException in case of invalid metadata */ - @Nullable - ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException; + @Nullable ManagedAttribute getManagedAttribute(Method method) throws InvalidMetadataException; /** - * Implementations should return an instance of {@code ManagedMetric} + * Implementations should return an instance of {@link ManagedMetric} * if the supplied {@code Method} has the corresponding metadata. - * Otherwise, should return {@code null}. - * @param method the method to read the attribute data from + * @param method the method to read the metric data from * @return the metric, or {@code null} if not found - * @throws InvalidMetadataException in case of invalid attributes + * @throws InvalidMetadataException in case of invalid metadata */ - @Nullable - ManagedMetric getManagedMetric(Method method) throws InvalidMetadataException; + @Nullable ManagedMetric getManagedMetric(Method method) throws InvalidMetadataException; /** - * Implementations should return an instance of {@code ManagedOperation} + * Implementations should return an instance of {@link ManagedOperation} * if the supplied {@code Method} has the corresponding metadata. - * Otherwise, should return {@code null}. - * @param method the method to read the attribute data from - * @return the attribute, or {@code null} if not found - * @throws InvalidMetadataException in case of invalid attributes + * @param method the method to read the operation data from + * @return the operation, or {@code null} if not found + * @throws InvalidMetadataException in case of invalid metadata */ - @Nullable - ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException; + @Nullable ManagedOperation getManagedOperation(Method method) throws InvalidMetadataException; /** - * Implementations should return an array of {@code ManagedOperationParameter} - * if the supplied {@code Method} has the corresponding metadata. Otherwise, - * should return an empty array if no metadata is found. + * Implementations should return an array of {@link ManagedOperationParameter + * ManagedOperationParameters} if the supplied {@code Method} has the corresponding + * metadata. * @param method the {@code Method} to read the metadata from - * @return the parameter information. - * @throws InvalidMetadataException in the case of invalid attributes. + * @return the parameter information, or an empty array if no metadata is found + * @throws InvalidMetadataException in case of invalid metadata */ - ManagedOperationParameter[] getManagedOperationParameters(Method method) throws InvalidMetadataException; + @Nullable ManagedOperationParameter[] getManagedOperationParameters(Method method) throws InvalidMetadataException; /** * Implementations should return an array of {@link ManagedNotification ManagedNotifications} - * if the supplied {@code Class} has the corresponding metadata. Otherwise, - * should return an empty array. + * if the supplied {@code Class} has the corresponding metadata. * @param clazz the {@code Class} to read the metadata from - * @return the notification information - * @throws InvalidMetadataException in the case of invalid metadata + * @return the notification information, or an empty array if no metadata is found + * @throws InvalidMetadataException in case of invalid metadata */ - ManagedNotification[] getManagedNotifications(Class clazz) throws InvalidMetadataException; - - + @Nullable ManagedNotification[] getManagedNotifications(Class clazz) throws InvalidMetadataException; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java index d7139b411d17..947aebcb432b 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedAttribute.java @@ -16,7 +16,7 @@ package org.springframework.jmx.export.metadata; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Metadata that indicates to expose a given bean property as JMX attribute. @@ -35,11 +35,9 @@ public class ManagedAttribute extends AbstractJmxAttribute { public static final ManagedAttribute EMPTY = new ManagedAttribute(); - @Nullable - private Object defaultValue; + private @Nullable Object defaultValue; - @Nullable - private String persistPolicy; + private @Nullable String persistPolicy; private int persistPeriod = -1; @@ -54,8 +52,7 @@ public void setDefaultValue(@Nullable Object defaultValue) { /** * Return the default value of this attribute. */ - @Nullable - public Object getDefaultValue() { + public @Nullable Object getDefaultValue() { return this.defaultValue; } @@ -63,8 +60,7 @@ public void setPersistPolicy(@Nullable String persistPolicy) { this.persistPolicy = persistPolicy; } - @Nullable - public String getPersistPolicy() { + public @Nullable String getPersistPolicy() { return this.persistPolicy; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java index 495fb5c24b5b..b3b9fd122198 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedMetric.java @@ -16,8 +16,9 @@ package org.springframework.jmx.export.metadata; +import org.jspecify.annotations.Nullable; + import org.springframework.jmx.support.MetricType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -31,21 +32,17 @@ */ public class ManagedMetric extends AbstractJmxAttribute { - @Nullable - private String category; + private @Nullable String category; - @Nullable - private String displayName; + private @Nullable String displayName; private MetricType metricType = MetricType.GAUGE; private int persistPeriod = -1; - @Nullable - private String persistPolicy; + private @Nullable String persistPolicy; - @Nullable - private String unit; + private @Nullable String unit; /** @@ -58,8 +55,7 @@ public void setCategory(@Nullable String category) { /** * The category of this metric (ex. throughput, performance, utilization). */ - @Nullable - public String getCategory() { + public @Nullable String getCategory() { return this.category; } @@ -73,8 +69,7 @@ public void setDisplayName(@Nullable String displayName) { /** * A display name for this metric. */ - @Nullable - public String getDisplayName() { + public @Nullable String getDisplayName() { return this.displayName; } @@ -117,8 +112,7 @@ public void setPersistPolicy(@Nullable String persistPolicy) { /** * The persist policy for this metric. */ - @Nullable - public String getPersistPolicy() { + public @Nullable String getPersistPolicy() { return this.persistPolicy; } @@ -132,8 +126,7 @@ public void setUnit(@Nullable String unit) { /** * The expected unit of measurement values. */ - @Nullable - public String getUnit() { + public @Nullable String getUnit() { return this.unit; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java index 3512b26bb2e0..5446d806ffa7 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedNotification.java @@ -16,7 +16,8 @@ package org.springframework.jmx.export.metadata; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -27,14 +28,11 @@ */ public class ManagedNotification { - @Nullable - private String[] notificationTypes; + private String @Nullable [] notificationTypes; - @Nullable - private String name; + private @Nullable String name; - @Nullable - private String description; + private @Nullable String description; /** @@ -48,15 +46,14 @@ public void setNotificationType(String notificationType) { /** * Set a list of notification types. */ - public void setNotificationTypes(@Nullable String... notificationTypes) { + public void setNotificationTypes(String @Nullable ... notificationTypes) { this.notificationTypes = notificationTypes; } /** * Return the list of notification types. */ - @Nullable - public String[] getNotificationTypes() { + public String @Nullable [] getNotificationTypes() { return this.notificationTypes; } @@ -70,8 +67,7 @@ public void setName(@Nullable String name) { /** * Return the name of this notification. */ - @Nullable - public String getName() { + public @Nullable String getName() { return this.name; } @@ -85,8 +81,7 @@ public void setDescription(@Nullable String description) { /** * Return a description for this notification. */ - @Nullable - public String getDescription() { + public @Nullable String getDescription() { return this.description; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java index e85bda8c7d2f..4d99558fcf8d 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/ManagedResource.java @@ -16,7 +16,7 @@ package org.springframework.jmx.export.metadata; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Metadata indicating that instances of an annotated class @@ -31,24 +31,19 @@ */ public class ManagedResource extends AbstractJmxAttribute { - @Nullable - private String objectName; + private @Nullable String objectName; private boolean log = false; - @Nullable - private String logFile; + private @Nullable String logFile; - @Nullable - private String persistPolicy; + private @Nullable String persistPolicy; private int persistPeriod = -1; - @Nullable - private String persistName; + private @Nullable String persistName; - @Nullable - private String persistLocation; + private @Nullable String persistLocation; /** @@ -61,8 +56,7 @@ public void setObjectName(@Nullable String objectName) { /** * Return the JMX ObjectName of this managed resource. */ - @Nullable - public String getObjectName() { + public @Nullable String getObjectName() { return this.objectName; } @@ -78,8 +72,7 @@ public void setLogFile(@Nullable String logFile) { this.logFile = logFile; } - @Nullable - public String getLogFile() { + public @Nullable String getLogFile() { return this.logFile; } @@ -87,8 +80,7 @@ public void setPersistPolicy(@Nullable String persistPolicy) { this.persistPolicy = persistPolicy; } - @Nullable - public String getPersistPolicy() { + public @Nullable String getPersistPolicy() { return this.persistPolicy; } @@ -104,8 +96,7 @@ public void setPersistName(@Nullable String persistName) { this.persistName = persistName; } - @Nullable - public String getPersistName() { + public @Nullable String getPersistName() { return this.persistName; } @@ -113,8 +104,7 @@ public void setPersistLocation(@Nullable String persistLocation) { this.persistLocation = persistLocation; } - @Nullable - public String getPersistLocation() { + public @Nullable String getPersistLocation() { return this.persistLocation; } diff --git a/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java index 1163b2280f26..2edf716cb82d 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/metadata/package-info.java @@ -2,9 +2,7 @@ * Provides generic JMX metadata classes and basic support for reading * JMX metadata in a provider-agnostic manner. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.export.metadata; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java index 3c8aafe62349..3bc1221a358f 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/IdentityNamingStrategy.java @@ -21,8 +21,9 @@ import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import org.jspecify.annotations.Nullable; + import org.springframework.jmx.support.ObjectNameManager; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java index 60edf694fb34..5a145df7e0c5 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/KeyNamingStrategy.java @@ -24,12 +24,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PropertiesLoaderUtils; import org.springframework.jmx.support.ObjectNameManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -62,23 +62,20 @@ public class KeyNamingStrategy implements ObjectNamingStrategy, InitializingBean /** * Stores the mappings of bean key to {@code ObjectName}. */ - @Nullable - private Properties mappings; + private @Nullable Properties mappings; /** * Stores the {@code Resource}s containing properties that should be loaded * into the final merged set of {@code Properties} used for {@code ObjectName} * resolution. */ - @Nullable - private Resource[] mappingLocations; + private Resource @Nullable [] mappingLocations; /** * Stores the result of merging the {@code mappings} {@code Properties} * with the properties stored in the resources defined by {@code mappingLocations}. */ - @Nullable - private Properties mergedMappings; + private @Nullable Properties mergedMappings; /** diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java index ea4792a14b51..4d97025ff211 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/MetadataNamingStrategy.java @@ -21,12 +21,13 @@ import javax.management.MalformedObjectNameException; import javax.management.ObjectName; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.jmx.export.metadata.JmxAttributeSource; import org.springframework.jmx.export.metadata.ManagedResource; import org.springframework.jmx.support.ObjectNameManager; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -56,11 +57,9 @@ public class MetadataNamingStrategy implements ObjectNamingStrategy, Initializin /** * The {@code JmxAttributeSource} implementation to use for reading metadata. */ - @Nullable - private JmxAttributeSource attributeSource; + private @Nullable JmxAttributeSource attributeSource; - @Nullable - private String defaultDomain; + private @Nullable String defaultDomain; /** diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java index 8a75e8d702cb..7616732af0c9 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/ObjectNamingStrategy.java @@ -19,7 +19,7 @@ import javax.management.MalformedObjectNameException; import javax.management.ObjectName; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface that encapsulates the creation of {@code ObjectName} instances. diff --git a/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java index 98056c29034f..a47fbcd74c41 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/naming/package-info.java @@ -2,9 +2,7 @@ * Provides a strategy for ObjectName creation. Used by MBeanExporter * to determine the JMX names to use for exported Spring-managed beans. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.export.naming; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java index 10056eb1327e..97aa54e3e5e4 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/notification/package-info.java @@ -2,9 +2,7 @@ * Provides supporting infrastructure to allow Spring-created MBeans * to send JMX notifications. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.export.notification; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/export/package-info.java b/spring-context/src/main/java/org/springframework/jmx/export/package-info.java index 5adaaf68c7ee..6aec88a4966c 100644 --- a/spring-context/src/main/java/org/springframework/jmx/export/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/export/package-info.java @@ -2,9 +2,7 @@ * This package provides declarative creation and registration of * Spring-managed beans as JMX MBeans. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.export; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/package-info.java b/spring-context/src/main/java/org/springframework/jmx/package-info.java index 65922f4b756e..4e0d43ecc40b 100644 --- a/spring-context/src/main/java/org/springframework/jmx/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/package-info.java @@ -2,9 +2,7 @@ * This package contains Spring's JMX support, which includes registration of * Spring-managed beans as JMX MBeans as well as access to remote JMX MBeans. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java index 799c5c8cbb3c..475e1ac65de6 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/ConnectorServerFactoryBean.java @@ -30,11 +30,12 @@ import javax.management.remote.JMXServiceURL; import javax.management.remote.MBeanServerForwarder; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.jmx.JmxException; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -65,18 +66,15 @@ public class ConnectorServerFactoryBean extends MBeanRegistrationSupport private final Map environment = new HashMap<>(); - @Nullable - private MBeanServerForwarder forwarder; + private @Nullable MBeanServerForwarder forwarder; - @Nullable - private ObjectName objectName; + private @Nullable ObjectName objectName; private boolean threaded = false; private boolean daemon = false; - @Nullable - private JMXConnectorServer connectorServer; + private @Nullable JMXConnectorServer connectorServer; /** @@ -207,8 +205,7 @@ public void run() { @Override - @Nullable - public JMXConnectorServer getObject() { + public @Nullable JMXConnectorServer getObject() { return this.connectorServer; } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java index 43cb7e719ace..6b39dd7bdda4 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/JmxUtils.java @@ -32,9 +32,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jmx.MBeanServerNotFoundException; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; @@ -136,8 +136,7 @@ public static MBeanServer locateMBeanServer(@Nullable String agentId) throws MBe * @return the parameter types as classes * @throws ClassNotFoundException if a parameter type could not be resolved */ - @Nullable - public static Class[] parameterInfoToTypes(@Nullable MBeanParameterInfo[] paramInfo) + public static Class @Nullable [] parameterInfoToTypes(MBeanParameterInfo @Nullable [] paramInfo) throws ClassNotFoundException { return parameterInfoToTypes(paramInfo, ClassUtils.getDefaultClassLoader()); @@ -151,9 +150,8 @@ public static Class[] parameterInfoToTypes(@Nullable MBeanParameterInfo[] par * @return the parameter types as classes * @throws ClassNotFoundException if a parameter type could not be resolved */ - @Nullable - public static Class[] parameterInfoToTypes( - @Nullable MBeanParameterInfo[] paramInfo, @Nullable ClassLoader classLoader) + public static Class @Nullable [] parameterInfoToTypes( + MBeanParameterInfo @Nullable [] paramInfo, @Nullable ClassLoader classLoader) throws ClassNotFoundException { Class[] types = null; @@ -273,8 +271,7 @@ public static boolean isMBean(@Nullable Class clazz) { * @param clazz the class to check * @return the Standard MBean interface for the given class */ - @Nullable - public static Class getMBeanInterface(@Nullable Class clazz) { + public static @Nullable Class getMBeanInterface(@Nullable Class clazz) { if (clazz == null || clazz.getSuperclass() == null) { return null; } @@ -295,8 +292,7 @@ public static Class getMBeanInterface(@Nullable Class clazz) { * @param clazz the class to check * @return whether there is an MXBean interface for the given class */ - @Nullable - public static Class getMXBeanInterface(@Nullable Class clazz) { + public static @Nullable Class getMXBeanInterface(@Nullable Class clazz) { if (clazz == null || clazz.getSuperclass() == null) { return null; } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java b/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java index 26c696d1f261..b187eb6bf8f5 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/MBeanRegistrationSupport.java @@ -28,8 +28,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -77,8 +77,7 @@ public class MBeanRegistrationSupport { /** * The {@code MBeanServer} instance being used to register beans. */ - @Nullable - protected MBeanServer server; + protected @Nullable MBeanServer server; /** * The beans that have been registered by this exporter. @@ -104,8 +103,7 @@ public void setServer(@Nullable MBeanServer server) { /** * Return the {@code MBeanServer} that the beans will be registered with. */ - @Nullable - public final MBeanServer getServer() { + public final @Nullable MBeanServer getServer() { return this.server; } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java index dfaf6d7afcaa..3759f61f9de1 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerConnectionFactoryBean.java @@ -27,6 +27,8 @@ import javax.management.remote.JMXConnectorFactory; import javax.management.remote.JMXServiceURL; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.target.AbstractLazyCreationTargetSource; @@ -34,7 +36,6 @@ import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -55,24 +56,19 @@ public class MBeanServerConnectionFactoryBean implements FactoryBean, BeanClassLoaderAware, InitializingBean, DisposableBean { - @Nullable - private JMXServiceURL serviceUrl; + private @Nullable JMXServiceURL serviceUrl; private final Map environment = new HashMap<>(); private boolean connectOnStartup = true; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private JMXConnector connector; + private @Nullable JMXConnector connector; - @Nullable - private MBeanServerConnection connection; + private @Nullable MBeanServerConnection connection; - @Nullable - private JMXConnectorLazyInitTargetSource connectorTargetSource; + private @Nullable JMXConnectorLazyInitTargetSource connectorTargetSource; /** @@ -159,8 +155,7 @@ private void createLazyConnection() { @Override - @Nullable - public MBeanServerConnection getObject() { + public @Nullable MBeanServerConnection getObject() { return this.connection; } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java index 259bc23b5008..930b9a4dbdbd 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/MBeanServerFactoryBean.java @@ -21,12 +21,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.jmx.MBeanServerNotFoundException; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} that obtains a {@link javax.management.MBeanServer} reference @@ -59,16 +59,13 @@ public class MBeanServerFactoryBean implements FactoryBean, Initial private boolean locateExistingServerIfPossible = false; - @Nullable - private String agentId; + private @Nullable String agentId; - @Nullable - private String defaultDomain; + private @Nullable String defaultDomain; private boolean registerWithFactory = true; - @Nullable - private MBeanServer server; + private @Nullable MBeanServer server; private boolean newlyRegistered = false; @@ -187,8 +184,7 @@ protected MBeanServer createMBeanServer(@Nullable String defaultDomain, boolean @Override - @Nullable - public MBeanServer getObject() { + public @Nullable MBeanServer getObject() { return this.server; } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java b/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java index 9244be32ced0..012cd6312c5d 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/NotificationListenerHolder.java @@ -26,7 +26,8 @@ import javax.management.NotificationListener; import javax.management.ObjectName; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** @@ -42,17 +43,13 @@ */ public class NotificationListenerHolder { - @Nullable - private NotificationListener notificationListener; + private @Nullable NotificationListener notificationListener; - @Nullable - private NotificationFilter notificationFilter; + private @Nullable NotificationFilter notificationFilter; - @Nullable - private Object handback; + private @Nullable Object handback; - @Nullable - protected Set mappedObjectNames; + protected @Nullable Set mappedObjectNames; /** @@ -65,8 +62,7 @@ public void setNotificationListener(@Nullable NotificationListener notificationL /** * Get the {@link javax.management.NotificationListener}. */ - @Nullable - public NotificationListener getNotificationListener() { + public @Nullable NotificationListener getNotificationListener() { return this.notificationListener; } @@ -84,8 +80,7 @@ public void setNotificationFilter(@Nullable NotificationFilter notificationFilte * with the encapsulated {@link #getNotificationListener() NotificationListener}. *

May be {@code null}. */ - @Nullable - public NotificationFilter getNotificationFilter() { + public @Nullable NotificationFilter getNotificationFilter() { return this.notificationFilter; } @@ -107,8 +102,7 @@ public void setHandback(@Nullable Object handback) { * @return the handback object (may be {@code null}) * @see javax.management.NotificationListener#handleNotification(javax.management.Notification, Object) */ - @Nullable - public Object getHandback() { + public @Nullable Object getHandback() { return this.handback; } @@ -141,8 +135,7 @@ public void setMappedObjectNames(Object... mappedObjectNames) { * be registered as a listener for {@link javax.management.Notification Notifications}. * @throws MalformedObjectNameException if an {@code ObjectName} is malformed */ - @Nullable - public ObjectName[] getResolvedObjectNames() throws MalformedObjectNameException { + public ObjectName @Nullable [] getResolvedObjectNames() throws MalformedObjectNameException { if (this.mappedObjectNames == null) { return null; } diff --git a/spring-context/src/main/java/org/springframework/jmx/support/package-info.java b/spring-context/src/main/java/org/springframework/jmx/support/package-info.java index d648547da279..1e287db019da 100644 --- a/spring-context/src/main/java/org/springframework/jmx/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/jmx/support/package-info.java @@ -2,9 +2,7 @@ * Contains support classes for connecting to local and remote {@code MBeanServer}s * and for exposing an {@code MBeanServer} to remote clients. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jmx.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java b/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java index 44ed724aff5f..ae20c8d0ed50 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiAccessor.java @@ -20,8 +20,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Convenient superclass for JNDI accessors, providing "jndiTemplate" @@ -70,8 +69,7 @@ public void setJndiEnvironment(@Nullable Properties jndiEnvironment) { /** * Return the JNDI environment to use for JNDI lookups. */ - @Nullable - public Properties getJndiEnvironment() { + public @Nullable Properties getJndiEnvironment() { return this.jndiTemplate.getEnvironment(); } diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java b/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java index cf53e20a47b1..36948c6b49ec 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiCallback.java @@ -19,7 +19,7 @@ import javax.naming.Context; import javax.naming.NamingException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Callback interface to be implemented by classes that need to perform an @@ -47,8 +47,7 @@ public interface JndiCallback { * @return a result object, or {@code null} * @throws NamingException if thrown by JNDI methods */ - @Nullable - T doInContext(Context ctx) throws NamingException; + @Nullable T doInContext(Context ctx) throws NamingException; } diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java index c46b2fa94038..ae34d6dc1158 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorDelegate.java @@ -19,8 +19,9 @@ import javax.naming.InitialContext; import javax.naming.NamingException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.SpringProperties; -import org.springframework.lang.Nullable; /** * {@link JndiLocatorSupport} subclass with public lookup methods, diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java index 7e255cc688c7..e2b5d5530dfd 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiLocatorSupport.java @@ -18,7 +18,8 @@ import javax.naming.NamingException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java index bba7d9b44a2d..60f36355d857 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectFactoryBean.java @@ -24,6 +24,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.SimpleTypeConverter; @@ -34,7 +35,6 @@ import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -73,8 +73,7 @@ public class JndiObjectFactoryBean extends JndiObjectLocator implements FactoryBean, BeanFactoryAware, BeanClassLoaderAware { - @Nullable - private Class[] proxyInterfaces; + private Class @Nullable [] proxyInterfaces; private boolean lookupOnStartup = true; @@ -82,17 +81,13 @@ public class JndiObjectFactoryBean extends JndiObjectLocator private boolean exposeAccessContext = false; - @Nullable - private Object defaultObject; + private @Nullable Object defaultObject; - @Nullable - private ConfigurableBeanFactory beanFactory; + private @Nullable ConfigurableBeanFactory beanFactory; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private Object jndiObject; + private @Nullable Object jndiObject; /** @@ -267,14 +262,12 @@ else if (logger.isDebugEnabled()) { * Return the singleton JNDI object. */ @Override - @Nullable - public Object getObject() { + public @Nullable Object getObject() { return this.jndiObject; } @Override - @Nullable - public Class getObjectType() { + public @Nullable Class getObjectType() { if (this.proxyInterfaces != null) { if (this.proxyInterfaces.length == 1) { return this.proxyInterfaces[0]; @@ -369,8 +362,7 @@ public JndiContextExposingInterceptor(JndiTemplate jndiTemplate) { } @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Context ctx = (isEligible(invocation.getMethod()) ? this.jndiTemplate.getContext() : null); try { return invocation.proceed(); diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java index 1bdecce4b925..fea6a6aab11e 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectLocator.java @@ -18,8 +18,9 @@ import javax.naming.NamingException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -49,11 +50,9 @@ */ public abstract class JndiObjectLocator extends JndiLocatorSupport implements InitializingBean { - @Nullable - private String jndiName; + private @Nullable String jndiName; - @Nullable - private Class expectedType; + private @Nullable Class expectedType; /** @@ -69,8 +68,7 @@ public void setJndiName(@Nullable String jndiName) { /** * Return the JNDI name to look up. */ - @Nullable - public String getJndiName() { + public @Nullable String getJndiName() { return this.jndiName; } @@ -86,8 +84,7 @@ public void setExpectedType(@Nullable Class expectedType) { * Return the type that the located JNDI object is supposed * to be assignable to, if any. */ - @Nullable - public Class getExpectedType() { + public @Nullable Class getExpectedType() { return this.expectedType; } diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java index cc8e392a7778..6afc62f3fd85 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiObjectTargetSource.java @@ -18,8 +18,9 @@ import javax.naming.NamingException; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.TargetSource; -import org.springframework.lang.Nullable; /** * AOP {@link org.springframework.aop.TargetSource} that provides @@ -65,11 +66,9 @@ public class JndiObjectTargetSource extends JndiObjectLocator implements TargetS private boolean cache = true; - @Nullable - private Object cachedObject; + private @Nullable Object cachedObject; - @Nullable - private Class targetClass; + private @Nullable Class targetClass; /** @@ -109,8 +108,7 @@ public void afterPropertiesSet() throws NamingException { @Override - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { if (this.cachedObject != null) { return this.cachedObject.getClass(); } @@ -128,8 +126,7 @@ public boolean isStatic() { } @Override - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { try { if (this.lookupOnStartup || !this.cache) { return (this.cachedObject != null ? this.cachedObject : lookup()); diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java b/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java index 88a4aa4c06f0..31efaff38613 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiPropertySource.java @@ -18,8 +18,9 @@ import javax.naming.NamingException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.PropertySource; -import org.springframework.lang.Nullable; /** * {@link PropertySource} implementation that reads properties from an underlying Spring @@ -78,8 +79,7 @@ public JndiPropertySource(String name, JndiLocatorDelegate jndiLocator) { * {@code null} and issues a DEBUG-level log statement with the exception message. */ @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { if (getSource().isResourceRef() && name.indexOf(':') != -1) { // We're in resource-ref (prefixing with "java:comp/env") mode. Let's not bother // with property names with a colon it since they're probably just containing a diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java b/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java index e08829e70c73..5936b42158e7 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiTemplate.java @@ -26,8 +26,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; /** @@ -44,8 +44,7 @@ public class JndiTemplate { protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Properties environment; + private @Nullable Properties environment; /** @@ -72,8 +71,7 @@ public void setEnvironment(@Nullable Properties environment) { /** * Return the environment for the JNDI InitialContext, if any. */ - @Nullable - public Properties getEnvironment() { + public @Nullable Properties getEnvironment() { return this.environment; } @@ -85,8 +83,7 @@ public Properties getEnvironment() { * @throws NamingException thrown by the callback implementation * @see #createInitialContext */ - @Nullable - public T execute(JndiCallback contextCallback) throws NamingException { + public @Nullable T execute(JndiCallback contextCallback) throws NamingException { Context ctx = getContext(); try { return contextCallback.doInContext(ctx); diff --git a/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java b/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java index a5931d894f9f..b64874545ed5 100644 --- a/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java +++ b/spring-context/src/main/java/org/springframework/jndi/JndiTemplateEditor.java @@ -19,8 +19,9 @@ import java.beans.PropertyEditorSupport; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.propertyeditors.PropertiesEditor; -import org.springframework.lang.Nullable; /** * Properties editor for JndiTemplate objects. Allows properties of type diff --git a/spring-context/src/main/java/org/springframework/jndi/package-info.java b/spring-context/src/main/java/org/springframework/jndi/package-info.java index 1ef8b64ac15a..1c833c48687f 100644 --- a/spring-context/src/main/java/org/springframework/jndi/package-info.java +++ b/spring-context/src/main/java/org/springframework/jndi/package-info.java @@ -7,9 +7,7 @@ * Expert One-On-One J2EE Design and Development * by Rod Johnson (Wrox, 2002). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jndi; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java index beb548986386..ba75eeb7150a 100644 --- a/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java +++ b/spring-context/src/main/java/org/springframework/jndi/support/SimpleJndiBeanFactory.java @@ -25,6 +25,8 @@ import javax.naming.NameNotFoundException; import javax.naming.NamingException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanDefinitionStoreException; import org.springframework.beans.factory.BeanFactory; @@ -35,7 +37,6 @@ import org.springframework.core.ResolvableType; import org.springframework.jndi.JndiLocatorSupport; import org.springframework.jndi.TypeMismatchNamingException; -import org.springframework.lang.Nullable; /** * Simple JNDI-based implementation of Spring's @@ -131,7 +132,7 @@ public T getBean(String name, Class requiredType) throws BeansException { } @Override - public Object getBean(String name, @Nullable Object... args) throws BeansException { + public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException { if (args != null) { throw new UnsupportedOperationException( "SimpleJndiBeanFactory does not support explicit bean creation arguments"); @@ -145,7 +146,7 @@ public T getBean(Class requiredType) throws BeansException { } @Override - public T getBean(Class requiredType, @Nullable Object... args) throws BeansException { + public T getBean(Class requiredType, @Nullable Object @Nullable ... args) throws BeansException { if (args != null) { throw new UnsupportedOperationException( "SimpleJndiBeanFactory does not support explicit bean creation arguments"); @@ -161,12 +162,11 @@ public T getObject() throws BeansException { return getBean(requiredType); } @Override - public T getObject(Object... args) throws BeansException { + public T getObject(@Nullable Object... args) throws BeansException { return getBean(requiredType, args); } @Override - @Nullable - public T getIfAvailable() throws BeansException { + public @Nullable T getIfAvailable() throws BeansException { try { return getBean(requiredType); } @@ -178,8 +178,7 @@ public T getIfAvailable() throws BeansException { } } @Override - @Nullable - public T getIfUnique() throws BeansException { + public @Nullable T getIfUnique() throws BeansException { try { return getBean(requiredType); } @@ -233,14 +232,12 @@ public boolean isTypeMatch(String name, @Nullable Class typeToMatch) throws N } @Override - @Nullable - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { return getType(name, true); } @Override - @Nullable - public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { try { return doGetType(name); } diff --git a/spring-context/src/main/java/org/springframework/jndi/support/package-info.java b/spring-context/src/main/java/org/springframework/jndi/support/package-info.java index 3669b23b625f..ba21e4d4576b 100644 --- a/spring-context/src/main/java/org/springframework/jndi/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/jndi/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for JNDI usage, * including a JNDI-based BeanFactory implementation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jndi.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java index e1b2349ea84b..bee6db255972 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/SchedulingAwareRunnable.java @@ -16,7 +16,7 @@ package org.springframework.scheduling; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Extension of the {@link Runnable} interface, adding special callbacks @@ -58,8 +58,7 @@ default boolean isLongLived() { * @since 6.1 * @see org.springframework.scheduling.annotation.Scheduled#scheduler() */ - @Nullable - default String getQualifier() { + default @Nullable String getQualifier() { return null; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java index a25739772fea..07b77bd64c7c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TaskScheduler.java @@ -22,7 +22,7 @@ import java.util.Date; import java.util.concurrent.ScheduledFuture; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Task scheduler interface that abstracts the scheduling of @@ -75,8 +75,7 @@ default Clock getClock() { * for internal reasons (for example, a pool overload handling policy or a pool shutdown in progress) * @see org.springframework.scheduling.support.CronTrigger */ - @Nullable - ScheduledFuture schedule(Runnable task, Trigger trigger); + @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger); /** * Schedule the given {@link Runnable}, invoking it at the specified execution time. diff --git a/spring-context/src/main/java/org/springframework/scheduling/Trigger.java b/spring-context/src/main/java/org/springframework/scheduling/Trigger.java index 4739ecf2b43e..5de2fca11835 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/Trigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/Trigger.java @@ -19,7 +19,7 @@ import java.time.Instant; import java.util.Date; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Common interface for trigger objects that determine the next execution time @@ -42,8 +42,7 @@ public interface Trigger { * @deprecated as of 6.0, in favor of {@link #nextExecution(TriggerContext)} */ @Deprecated(since = "6.0") - @Nullable - default Date nextExecutionTime(TriggerContext triggerContext) { + default @Nullable Date nextExecutionTime(TriggerContext triggerContext) { Instant instant = nextExecution(triggerContext); return (instant != null ? Date.from(instant) : null); } @@ -56,7 +55,6 @@ default Date nextExecutionTime(TriggerContext triggerContext) { * or {@code null} if the trigger won't fire anymore * @since 6.0 */ - @Nullable - Instant nextExecution(TriggerContext triggerContext); + @Nullable Instant nextExecution(TriggerContext triggerContext); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java index d2fc96d677d0..5db303df142d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java +++ b/spring-context/src/main/java/org/springframework/scheduling/TriggerContext.java @@ -20,7 +20,7 @@ import java.time.Instant; import java.util.Date; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Context object encapsulating last execution times and last completion time @@ -48,9 +48,8 @@ default Clock getClock() { *

The default implementation delegates to {@link #lastScheduledExecution()}. * @deprecated as of 6.0, in favor on {@link #lastScheduledExecution()} */ - @Nullable @Deprecated(since = "6.0") - default Date lastScheduledExecutionTime() { + default @Nullable Date lastScheduledExecutionTime() { Instant instant = lastScheduledExecution(); return (instant != null ? Date.from(instant) : null); } @@ -60,8 +59,7 @@ default Date lastScheduledExecutionTime() { * or {@code null} if not scheduled before. * @since 6.0 */ - @Nullable - Instant lastScheduledExecution(); + @Nullable Instant lastScheduledExecution(); /** * Return the last actual execution time of the task, @@ -69,9 +67,8 @@ default Date lastScheduledExecutionTime() { *

The default implementation delegates to {@link #lastActualExecution()}. * @deprecated as of 6.0, in favor on {@link #lastActualExecution()} */ - @Nullable @Deprecated(since = "6.0") - default Date lastActualExecutionTime() { + default @Nullable Date lastActualExecutionTime() { Instant instant = lastActualExecution(); return (instant != null ? Date.from(instant) : null); } @@ -81,8 +78,7 @@ default Date lastActualExecutionTime() { * or {@code null} if not scheduled before. * @since 6.0 */ - @Nullable - Instant lastActualExecution(); + @Nullable Instant lastActualExecution(); /** * Return the last completion time of the task, @@ -91,8 +87,7 @@ default Date lastActualExecutionTime() { * @deprecated as of 6.0, in favor on {@link #lastCompletion()} */ @Deprecated(since = "6.0") - @Nullable - default Date lastCompletionTime() { + default @Nullable Date lastCompletionTime() { Instant instant = lastCompletion(); return (instant != null ? Date.from(instant) : null); } @@ -102,7 +97,6 @@ default Date lastCompletionTime() { * or {@code null} if not scheduled before. * @since 6.0 */ - @Nullable - Instant lastCompletion(); + @Nullable Instant lastCompletion(); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java index 35e08bec19e7..b5b144210ba8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AbstractAsyncConfiguration.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. @@ -21,6 +21,8 @@ import java.util.function.Function; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.beans.factory.ObjectProvider; import org.springframework.beans.factory.annotation.Autowired; @@ -28,7 +30,6 @@ import org.springframework.context.annotation.ImportAware; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.function.SingletonSupplier; @@ -45,14 +46,11 @@ @Configuration(proxyBeanMethods = false) public abstract class AbstractAsyncConfiguration implements ImportAware { - @Nullable - protected AnnotationAttributes enableAsync; + protected @Nullable AnnotationAttributes enableAsync; - @Nullable - protected Supplier executor; + protected @Nullable Supplier executor; - @Nullable - protected Supplier exceptionHandler; + protected @Nullable Supplier exceptionHandler; @Override @@ -69,8 +67,9 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { * Collect any {@link AsyncConfigurer} beans through autowiring. */ @Autowired + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1128 void setConfigurers(ObjectProvider configurers) { - Supplier configurer = SingletonSupplier.of(() -> { + SingletonSupplier configurer = SingletonSupplier.ofNullable(() -> { List candidates = configurers.stream().toList(); if (CollectionUtils.isEmpty(candidates)) { return null; @@ -84,7 +83,7 @@ void setConfigurers(ObjectProvider configurers) { this.exceptionHandler = adapt(configurer, AsyncConfigurer::getAsyncUncaughtExceptionHandler); } - private Supplier adapt(Supplier supplier, Function provider) { + private Supplier<@Nullable T> adapt(SingletonSupplier supplier, Function provider) { return () -> { AsyncConfigurer configurer = supplier.get(); return (configurer != null ? provider.apply(configurer) : null); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java index ae9a987b5282..8b3f44c35a88 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AnnotationAsyncExecutionInterceptor.java @@ -19,10 +19,11 @@ import java.lang.reflect.Method; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.interceptor.AsyncExecutionInterceptor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; /** * Specialization of {@link AsyncExecutionInterceptor} that delegates method execution to @@ -79,8 +80,7 @@ public AnnotationAsyncExecutionInterceptor(@Nullable Executor defaultExecutor, A * @see #determineAsyncExecutor(Method) */ @Override - @Nullable - protected String getExecutorQualifier(Method method) { + protected @Nullable String getExecutorQualifier(Method method) { // Maintainer's note: changes made here should also be made in // AnnotationAsyncExecutionAspect#getExecutorQualifier Async async = AnnotatedElementUtils.findMergedAnnotation(method, Async.class); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java index f88cd4acf281..a957a8d76587 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationAdvisor.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 java.util.function.Supplier; import org.aopalliance.aop.Advice; +import org.jspecify.annotations.Nullable; import org.springframework.aop.Pointcut; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; @@ -31,7 +32,6 @@ import org.springframework.aop.support.annotation.AnnotationMatchingPointcut; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -64,7 +64,7 @@ public class AsyncAnnotationAdvisor extends AbstractPointcutAdvisor implements B * Create a new {@code AsyncAnnotationAdvisor} for bean-style configuration. */ public AsyncAnnotationAdvisor() { - this((Supplier) null, (Supplier) null); + this((Supplier) null, (Supplier) null); } /** @@ -92,7 +92,7 @@ public AsyncAnnotationAdvisor( */ @SuppressWarnings("unchecked") public AsyncAnnotationAdvisor( - @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { Set> asyncAnnotationTypes = CollectionUtils.newLinkedHashSet(2); asyncAnnotationTypes.add(Async.class); @@ -157,7 +157,7 @@ public Pointcut getPointcut() { protected Advice buildAdvice( - @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { + @Nullable Supplier executor, @Nullable Supplier exceptionHandler) { AnnotationAsyncExecutionInterceptor interceptor = new AnnotationAsyncExecutionInterceptor(null); interceptor.configure(executor, exceptionHandler); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java index 9d8f801590eb..8be83b3f83af 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessor.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,12 +22,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.autoproxy.AbstractBeanFactoryAwareAdvisingPostProcessor; import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.task.TaskExecutor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; @@ -77,14 +77,11 @@ public class AsyncAnnotationBeanPostProcessor extends AbstractBeanFactoryAwareAd protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Supplier executor; + private @Nullable Supplier executor; - @Nullable - private Supplier exceptionHandler; + private @Nullable Supplier exceptionHandler; - @Nullable - private Class asyncAnnotationType; + private @Nullable Class asyncAnnotationType; @@ -98,8 +95,8 @@ public AsyncAnnotationBeanPostProcessor() { * applying the corresponding default if a supplier is not resolvable. * @since 5.1 */ - public void configure(@Nullable Supplier executor, - @Nullable Supplier exceptionHandler) { + public void configure(@Nullable Supplier executor, + @Nullable Supplier exceptionHandler) { this.executor = executor; this.exceptionHandler = exceptionHandler; diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java index c7f50aed70a8..4f81076993d7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurationSelector.java @@ -18,7 +18,6 @@ import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AdviceModeImportSelector; -import org.springframework.lang.NonNull; /** * Selects which implementation of {@link AbstractAsyncConfiguration} should @@ -43,7 +42,6 @@ public class AsyncConfigurationSelector extends AdviceModeImportSelector new String[] {ProxyAsyncConfiguration.class.getName()}; diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java index 488297171ba6..a840df3fe3db 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurer.java @@ -18,8 +18,9 @@ import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.lang.Nullable; /** * Interface to be implemented by @{@link org.springframework.context.annotation.Configuration @@ -42,8 +43,7 @@ public interface AsyncConfigurer { * The {@link Executor} instance to be used when processing async * method invocations. */ - @Nullable - default Executor getAsyncExecutor() { + default @Nullable Executor getAsyncExecutor() { return null; } @@ -52,8 +52,7 @@ default Executor getAsyncExecutor() { * when an exception is thrown during an asynchronous method execution * with {@code void} return type. */ - @Nullable - default AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + default @Nullable AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java index 4c1e83146873..4c2a0a95218d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncConfigurerSupport.java @@ -18,8 +18,9 @@ import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.interceptor.AsyncUncaughtExceptionHandler; -import org.springframework.lang.Nullable; /** * A convenience {@link AsyncConfigurer} that implements all methods @@ -34,14 +35,12 @@ public class AsyncConfigurerSupport implements AsyncConfigurer { @Override - @Nullable - public Executor getAsyncExecutor() { + public @Nullable Executor getAsyncExecutor() { return null; } @Override - @Nullable - public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { + public @Nullable AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() { return null; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java index 3a7b5f0812ca..7f9f39139906 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/AsyncResult.java @@ -21,21 +21,12 @@ import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; -import org.springframework.util.concurrent.FailureCallback; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureCallback; -import org.springframework.util.concurrent.SuccessCallback; +import org.jspecify.annotations.Nullable; /** * A pass-through {@code Future} handle that can be used for method signatures * which are declared with a {@code Future} return type for asynchronous execution. * - *

As of Spring 4.1, this class implements {@code ListenableFuture}, not just - * plain {@link java.util.concurrent.Future}, along with the corresponding support - * in {@code @Async} processing. As of 7.0, this will be turned back to a plain - * {@code Future} in order to focus on compatibility with existing common usage. - * * @author Juergen Hoeller * @author Rossen Stoyanchev * @since 3.0 @@ -46,14 +37,11 @@ * @deprecated as of 6.0, in favor of {@link CompletableFuture} */ @Deprecated(since = "6.0") -@SuppressWarnings("removal") -public class AsyncResult implements ListenableFuture { +public class AsyncResult implements Future { - @Nullable - private final V value; + private final @Nullable V value; - @Nullable - private final Throwable executionException; + private final @Nullable Throwable executionException; /** @@ -90,8 +78,7 @@ public boolean isDone() { } @Override - @Nullable - public V get() throws ExecutionException { + public @Nullable V get() throws ExecutionException { if (this.executionException != null) { throw (this.executionException instanceof ExecutionException execEx ? execEx : new ExecutionException(this.executionException)); @@ -100,43 +87,10 @@ public V get() throws ExecutionException { } @Override - @Nullable - public V get(long timeout, TimeUnit unit) throws ExecutionException { + public @Nullable V get(long timeout, TimeUnit unit) throws ExecutionException { return get(); } - @Override - public void addCallback(ListenableFutureCallback callback) { - addCallback(callback, callback); - } - - @Override - public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { - try { - if (this.executionException != null) { - failureCallback.onFailure(exposedException(this.executionException)); - } - else { - successCallback.onSuccess(this.value); - } - } - catch (Throwable ex) { - // Ignore - } - } - - @Override - public CompletableFuture completable() { - if (this.executionException != null) { - CompletableFuture completable = new CompletableFuture<>(); - completable.completeExceptionally(exposedException(this.executionException)); - return completable; - } - else { - return CompletableFuture.completedFuture(this.value); - } - } - /** * Create a new async result which exposes the given value from {@link Future#get()}. @@ -144,7 +98,7 @@ public CompletableFuture completable() { * @since 4.2 * @see Future#get() */ - public static org.springframework.util.concurrent.ListenableFuture forValue(V value) { + public static Future forValue(V value) { return new AsyncResult<>(value, null); } @@ -156,24 +110,8 @@ public static org.springframework.util.concurrent.ListenableFuture forVal * @since 4.2 * @see ExecutionException */ - public static org.springframework.util.concurrent.ListenableFuture forExecutionException(Throwable ex) { + public static Future forExecutionException(Throwable ex) { return new AsyncResult<>(null, ex); } - /** - * Determine the exposed exception: either the cause of a given - * {@link ExecutionException}, or the original exception as-is. - * @param original the original as given to {@link #forExecutionException} - * @return the exposed exception - */ - private static Throwable exposedException(Throwable original) { - if (original instanceof ExecutionException) { - Throwable cause = original.getCause(); - if (cause != null) { - return cause; - } - } - return original; - } - } 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 38f5c2f410cb..cced791405ec 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 @@ * 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/ScheduledAnnotationBeanPostProcessor.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java index 17178ba0f32e..060e4191e590 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessor.java @@ -33,6 +33,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.AopProxyUtils; @@ -61,7 +62,6 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.format.annotation.DurationFormat; import org.springframework.format.datetime.standard.DurationFormatterUtils; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.config.CronTask; @@ -134,23 +134,17 @@ public class ScheduledAnnotationBeanPostProcessor private final ScheduledTaskRegistrar registrar; - @Nullable - private Object scheduler; + private @Nullable Object scheduler; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private TaskSchedulerRouter localScheduler; + private @Nullable TaskSchedulerRouter localScheduler; private final Set> nonAnnotatedClasses = ConcurrentHashMap.newKeySet(64); @@ -554,8 +548,7 @@ protected Runnable createRunnable(Object target, Method method, @Nullable String * @deprecated in favor of {@link #createRunnable(Object, Method, String)} */ @Deprecated(since = "6.1") - @Nullable - protected Runnable createRunnable(Object target, Method method) { + protected @Nullable Runnable createRunnable(Object target, Method method) { return null; } 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 a51af6a215ea..874afca7d86b 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 @@ -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,6 +27,7 @@ import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -37,7 +38,6 @@ import org.springframework.core.KotlinDetector; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.scheduling.support.DefaultScheduledTaskObservationConvention; import org.springframework.scheduling.support.ScheduledTaskObservationContext; @@ -81,7 +81,7 @@ abstract class ScheduledAnnotationReactiveSupport { * Kotlin coroutines bridge are not present at runtime */ public static boolean isReactive(Method method) { - if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(method)) { + if (KotlinDetector.isSuspendingFunction(method)) { // Note that suspending functions declared without args have a single Continuation // parameter in reflective inspection Assert.isTrue(method.getParameterCount() == 1, @@ -138,7 +138,7 @@ public static Runnable createSubscriptionRunnable(Method method, Object targetBe * to a {@code Flux} with a checkpoint String, allowing for better debugging. */ static Publisher getPublisherFor(Method method, Object bean) { - if (KotlinDetector.isKotlinPresent() && KotlinDetector.isSuspendingFunction(method)) { + if (KotlinDetector.isSuspendingFunction(method)) { return CoroutinesUtils.invokeSuspendingFunction(method, bean, (Object[]) method.getParameters()); } @@ -195,8 +195,7 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { final String displayName; - @Nullable - private final String qualifier; + private final @Nullable String qualifier; private final List subscriptionTrackerRegistry; @@ -205,9 +204,9 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { 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; @@ -219,8 +218,7 @@ static final class SubscribingRunnable implements SchedulingAwareRunnable { } @Override - @Nullable - public String getQualifier() { + public @Nullable String getQualifier() { return this.qualifier; } @@ -236,7 +234,7 @@ public void run() { latch.await(); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } } else { @@ -276,15 +274,13 @@ private static final class TrackingSubscriber implements Subscriber, Run private final Observation observation; - @Nullable - private final CountDownLatch blockingLatch; + private final @Nullable CountDownLatch blockingLatch; // Implementation note: since this is created last-minute when subscribing, // there shouldn't be a way to cancel the tracker externally from the // ScheduledAnnotationBeanProcessor before the #setSubscription(Subscription) // method is called. - @Nullable - private Subscription subscription; + private @Nullable Subscription subscription; TrackingSubscriber(List subscriptionTrackerRegistry, Observation observation) { this(subscriptionTrackerRegistry, observation, null); diff --git a/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java index e876e68edf2e..8247daf25a05 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java +++ b/spring-context/src/main/java/org/springframework/scheduling/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotation support for asynchronous method execution. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 d4a503d0c185..a11ca7d04a00 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. @@ -25,15 +25,14 @@ import jakarta.enterprise.concurrent.ManagedExecutors; import jakarta.enterprise.concurrent.ManagedTask; +import org.jspecify.annotations.Nullable; -import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.support.TaskExecutorAdapter; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.util.ClassUtils; -import org.springframework.util.concurrent.ListenableFuture; /** * Adapter that takes a {@code java.util.concurrent.Executor} and exposes @@ -62,15 +61,14 @@ * @see DefaultManagedTaskExecutor * @see ThreadPoolTaskExecutor */ -@SuppressWarnings({"deprecation", "removal"}) -public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { +@SuppressWarnings("deprecation") +public class ConcurrentTaskExecutor implements AsyncTaskExecutor, SchedulingTaskExecutor { private static final Executor STUB_EXECUTOR = (task -> { throw new IllegalStateException("Executor not configured"); }); - @Nullable - private static Class managedExecutorServiceClass; + private static @Nullable Class managedExecutorServiceClass; static { try { @@ -89,8 +87,7 @@ public class ConcurrentTaskExecutor implements AsyncListenableTaskExecutor, Sche private TaskExecutorAdapter adaptedExecutor = new TaskExecutorAdapter(STUB_EXECUTOR); - @Nullable - private TaskDecorator taskDecorator; + private @Nullable TaskDecorator taskDecorator; /** @@ -156,7 +153,7 @@ public void execute(Runnable task) { this.adaptedExecutor.execute(task); } - @Deprecated + @Deprecated(since = "5.3.16") @Override public void execute(Runnable task, long startTimeout) { this.adaptedExecutor.execute(task, startTimeout); @@ -172,16 +169,6 @@ public Future submit(Callable task) { return this.adaptedExecutor.submit(task); } - @Override - public ListenableFuture submitListenable(Runnable task) { - return this.adaptedExecutor.submitListenable(task); - } - - @Override - public ListenableFuture submitListenable(Callable task) { - return this.adaptedExecutor.submitListenable(task); - } - private TaskExecutorAdapter getAdaptedExecutor(Executor originalExecutor) { TaskExecutorAdapter adapter = @@ -224,16 +211,6 @@ public Future submit(Runnable task) { public Future submit(Callable task) { return super.submit(ManagedTaskBuilder.buildManagedTask(task, task.toString())); } - - @Override - public ListenableFuture submitListenable(Runnable task) { - return super.submitListenable(ManagedTaskBuilder.buildManagedTask(task, task.toString())); - } - - @Override - public ListenableFuture submitListenable(Callable task) { - return super.submitListenable(ManagedTaskBuilder.buildManagedTask(task, task.toString())); - } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java index f707d2bcc48e..2d2322b0b5af 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ConcurrentTaskScheduler.java @@ -31,9 +31,9 @@ import jakarta.enterprise.concurrent.LastExecution; import jakarta.enterprise.concurrent.ManagedScheduledExecutorService; +import org.jspecify.annotations.Nullable; import org.springframework.core.task.TaskRejectedException; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; @@ -75,8 +75,7 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T private static final TimeUnit NANO = TimeUnit.NANOSECONDS; - @Nullable - private static Class managedScheduledExecutorServiceClass; + private static @Nullable Class managedScheduledExecutorServiceClass; static { try { @@ -91,13 +90,11 @@ public class ConcurrentTaskScheduler extends ConcurrentTaskExecutor implements T } - @Nullable - private ScheduledExecutorService scheduledExecutor; + private @Nullable ScheduledExecutorService scheduledExecutor; private boolean enterpriseConcurrentScheduler = false; - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; private Clock clock = Clock.systemDefaultZone(); @@ -218,21 +215,8 @@ public Future submit(Callable task) { return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); } - @SuppressWarnings({"deprecation", "removal"}) @Override - public org.springframework.util.concurrent.ListenableFuture submitListenable(Runnable task) { - return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); - } - - @SuppressWarnings({"deprecation", "removal"}) - @Override - public org.springframework.util.concurrent.ListenableFuture submitListenable(Callable task) { - return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); - } - - @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { ScheduledExecutorService scheduleExecutorToUse = getScheduledExecutor(); try { if (this.enterpriseConcurrentScheduler) { @@ -344,8 +328,7 @@ public TriggerAdapter(Trigger adaptee) { } @Override - @Nullable - public Date getNextRunTime(@Nullable LastExecution le, Date taskScheduledTime) { + public @Nullable Date getNextRunTime(@Nullable LastExecution le, Date taskScheduledTime) { Instant instant = this.adaptee.nextExecution(new LastExecutionAdapter(le)); return (instant != null ? Date.from(instant) : null); } @@ -358,33 +341,28 @@ public boolean skipRun(LastExecution lastExecutionInfo, Date scheduledRunTime) { private static class LastExecutionAdapter implements TriggerContext { - @Nullable - private final LastExecution le; + private final @Nullable LastExecution le; public LastExecutionAdapter(@Nullable LastExecution le) { this.le = le; } @Override - @Nullable - public Instant lastScheduledExecution() { + public @Nullable Instant lastScheduledExecution() { return (this.le != null ? toInstant(this.le.getScheduledStart()) : null); } @Override - @Nullable - public Instant lastActualExecution() { + public @Nullable Instant lastActualExecution() { return (this.le != null ? toInstant(this.le.getRunStart()) : null); } @Override - @Nullable - public Instant lastCompletion() { + public @Nullable Instant lastCompletion() { return (this.le != null ? toInstant(this.le.getRunEnd()) : null); } - @Nullable - private static Instant toInstant(@Nullable Date date) { + private static @Nullable Instant toInstant(@Nullable Date date) { return (date != null ? date.toInstant() : null); } } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java index 417a542c6862..04e4dfd2adca 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DefaultManagedAwareThreadFactory.java @@ -23,11 +23,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.jndi.JndiLocatorDelegate; import org.springframework.jndi.JndiTemplate; -import org.springframework.lang.Nullable; /** * JNDI-based variant of {@link CustomizableThreadFactory}, performing a default lookup @@ -53,11 +53,9 @@ public class DefaultManagedAwareThreadFactory extends CustomizableThreadFactory private final JndiLocatorDelegate jndiLocator = new JndiLocatorDelegate(); - @Nullable - private String jndiName = "java:comp/DefaultManagedThreadFactory"; + private @Nullable String jndiName = "java:comp/DefaultManagedThreadFactory"; - @Nullable - private ThreadFactory threadFactory; + private @Nullable ThreadFactory threadFactory; /** diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java index c30f209c53f0..fd2c3ac3f8aa 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/DelegatingErrorHandlingCallable.java @@ -19,7 +19,8 @@ import java.lang.reflect.UndeclaredThrowableException; import java.util.concurrent.Callable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.support.TaskUtils; import org.springframework.util.ErrorHandler; import org.springframework.util.ReflectionUtils; @@ -46,8 +47,7 @@ public DelegatingErrorHandlingCallable(Callable delegate, @Nullable ErrorHand @Override - @Nullable - public V call() throws Exception { + public @Nullable V call() throws Exception { try { return this.delegate.call(); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java index 99f22d71a09c..9a0b820e6182 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorConfigurationSupport.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; @@ -38,7 +39,6 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.VirtualThreadTaskExecutor; -import org.springframework.lang.Nullable; /** * Base class for setting up a {@link java.util.concurrent.ExecutorService} @@ -93,17 +93,13 @@ public abstract class ExecutorConfigurationSupport extends CustomizableThreadFac private int phase = DEFAULT_PHASE; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private ExecutorService executor; + private @Nullable ExecutorService executor; - @Nullable - private ExecutorLifecycleDelegate lifecycleDelegate; + private @Nullable ExecutorLifecycleDelegate lifecycleDelegate; private volatile boolean lateShutdown; diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java index e8e705fc2179..3a49ca6ef288 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ExecutorLifecycleDelegate.java @@ -21,8 +21,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.jspecify.annotations.Nullable; + import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; /** * An internal delegate for common {@link ExecutorService} lifecycle management @@ -47,8 +48,7 @@ final class ExecutorLifecycleDelegate implements SmartLifecycle { private int executingTaskCount = 0; - @Nullable - private Runnable stopCallback; + private @Nullable Runnable stopCallback; public ExecutorLifecycleDelegate(ExecutorService executor) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java index 26b1165201fb..2de78626fdb8 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ForkJoinPoolFactoryBean.java @@ -19,10 +19,11 @@ import java.util.concurrent.ForkJoinPool; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * A Spring {@link FactoryBean} that builds and exposes a preconfigured {@link ForkJoinPool}. @@ -38,15 +39,13 @@ public class ForkJoinPoolFactoryBean implements FactoryBean, Initi private ForkJoinPool.ForkJoinWorkerThreadFactory threadFactory = ForkJoinPool.defaultForkJoinWorkerThreadFactory; - @Nullable - private Thread.UncaughtExceptionHandler uncaughtExceptionHandler; + private Thread.@Nullable UncaughtExceptionHandler uncaughtExceptionHandler; private boolean asyncMode = false; private int awaitTerminationSeconds = 0; - @Nullable - private ForkJoinPool forkJoinPool; + private @Nullable ForkJoinPool forkJoinPool; /** @@ -128,8 +127,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public ForkJoinPool getObject() { + public @Nullable ForkJoinPool getObject() { return this.forkJoinPool; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java index c14be23fd5cc..d5aab59378a0 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ReschedulingRunnable.java @@ -26,7 +26,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.Trigger; import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; import org.springframework.scheduling.support.SimpleTriggerContext; @@ -53,11 +54,9 @@ class ReschedulingRunnable extends DelegatingErrorHandlingRunnable implements Sc private final ScheduledExecutorService executor; - @Nullable - private ScheduledFuture currentFuture; + private @Nullable ScheduledFuture currentFuture; - @Nullable - private Instant scheduledExecutionTime; + private @Nullable Instant scheduledExecutionTime; private final Object triggerContextMonitor = new Object(); @@ -72,8 +71,7 @@ public ReschedulingRunnable(Runnable delegate, Trigger trigger, Clock clock, } - @Nullable - public ScheduledFuture schedule() { + public @Nullable ScheduledFuture schedule() { synchronized (this.triggerContextMonitor) { this.scheduledExecutionTime = this.trigger.nextExecution(this.triggerContext); if (this.scheduledExecutionTime == null) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java index c41101b49953..dfdf377df77b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorFactoryBean.java @@ -23,8 +23,9 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.ThreadFactory; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; import org.springframework.scheduling.support.TaskUtils; import org.springframework.util.Assert; @@ -76,8 +77,7 @@ public class ScheduledExecutorFactoryBean extends ExecutorConfigurationSupport private int poolSize = 1; - @Nullable - private ScheduledExecutorTask[] scheduledExecutorTasks; + private ScheduledExecutorTask @Nullable [] scheduledExecutorTasks; private boolean removeOnCancelPolicy = false; @@ -85,8 +85,7 @@ public class ScheduledExecutorFactoryBean extends ExecutorConfigurationSupport private boolean exposeUnconfigurableExecutor = false; - @Nullable - private ScheduledExecutorService exposedExecutor; + private @Nullable ScheduledExecutorService exposedExecutor; /** @@ -241,8 +240,7 @@ protected Runnable getRunnableToSchedule(ScheduledExecutorTask task) { @Override - @Nullable - public ScheduledExecutorService getObject() { + public @Nullable ScheduledExecutorService getObject() { return this.exposedExecutor; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java index 7e55e5eecfc3..dc2b55d4217f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ScheduledExecutorTask.java @@ -18,7 +18,8 @@ import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -41,8 +42,7 @@ */ public class ScheduledExecutorTask { - @Nullable - private Runnable runnable; + private @Nullable Runnable runnable; private long delay = 0; diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java index bb66d4512f17..21cb4cc68675 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskScheduler.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeUnit; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; @@ -37,7 +38,6 @@ import org.springframework.context.event.ContextClosedEvent; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskRejectedException; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; @@ -122,18 +122,15 @@ public class SimpleAsyncTaskScheduler extends SimpleAsyncTaskExecutor implements private final ExecutorLifecycleDelegate fixedDelayLifecycle = new ExecutorLifecycleDelegate(this.fixedDelayExecutor); - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; private Clock clock = Clock.systemDefaultZone(); private int phase = DEFAULT_PHASE; - @Nullable - private Executor targetTaskExecutor; + private @Nullable Executor targetTaskExecutor; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; /** @@ -269,21 +266,8 @@ public Future submit(Callable task) { return super.submit(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); } - @SuppressWarnings({"deprecation", "removal"}) @Override - public org.springframework.util.concurrent.ListenableFuture submitListenable(Runnable task) { - return super.submitListenable(TaskUtils.decorateTaskWithErrorHandler(task, this.errorHandler, false)); - } - - @SuppressWarnings({"deprecation", "removal"}) - @Override - public org.springframework.util.concurrent.ListenableFuture submitListenable(Callable task) { - return super.submitListenable(new DelegatingErrorHandlingCallable<>(task, this.errorHandler)); - } - - @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { try { Runnable delegate = scheduledTask(task); ErrorHandler errorHandler = diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java index 7680a2634f71..d57c979ecca3 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolExecutorFactoryBean.java @@ -26,8 +26,9 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.FactoryBean; -import org.springframework.lang.Nullable; /** * JavaBean that allows for configuring a {@link java.util.concurrent.ThreadPoolExecutor} @@ -81,8 +82,7 @@ public class ThreadPoolExecutorFactoryBean extends ExecutorConfigurationSupport private boolean exposeUnconfigurableExecutor = false; - @Nullable - private ExecutorService exposedExecutor; + private @Nullable ExecutorService exposedExecutor; /** @@ -245,8 +245,7 @@ protected void initiateEarlyShutdown() { @Override - @Nullable - public ExecutorService getObject() { + public @Nullable ExecutorService getObject() { return this.exposedExecutor; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java index 66e54c8fd602..f4999fb4314f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutor.java @@ -30,15 +30,14 @@ import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; -import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskRejectedException; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; /** * JavaBean that allows for configuring a {@link java.util.concurrent.ThreadPoolExecutor} @@ -80,9 +79,9 @@ * @see ThreadPoolExecutorFactoryBean * @see ConcurrentTaskExecutor */ -@SuppressWarnings({"serial", "deprecation", "removal"}) +@SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport - implements AsyncListenableTaskExecutor, SchedulingTaskExecutor { + implements AsyncTaskExecutor, SchedulingTaskExecutor { private final Object poolSizeMonitor = new Object(); @@ -100,11 +99,9 @@ public class ThreadPoolTaskExecutor extends ExecutorConfigurationSupport private boolean strictEarlyShutdown = false; - @Nullable - private TaskDecorator taskDecorator; + private @Nullable TaskDecorator taskDecorator; - @Nullable - private ThreadPoolExecutor threadPoolExecutor; + private @Nullable ThreadPoolExecutor threadPoolExecutor; // Runnable decorator to user-level FutureTask, if different private final Map decoratedTaskMap = @@ -414,32 +411,6 @@ public Future submit(Callable task) { } } - @Override - public ListenableFuture submitListenable(Runnable task) { - ExecutorService executor = getThreadPoolExecutor(); - try { - ListenableFutureTask future = new ListenableFutureTask<>(task, null); - executor.execute(future); - return future; - } - catch (RejectedExecutionException ex) { - throw new TaskRejectedException(executor, task, ex); - } - } - - @Override - public ListenableFuture submitListenable(Callable task) { - ExecutorService executor = getThreadPoolExecutor(); - try { - ListenableFutureTask future = new ListenableFutureTask<>(task); - executor.execute(future); - return future; - } - catch (RejectedExecutionException ex) { - throw new TaskRejectedException(executor, task, ex); - } - } - @Override protected void cancelRemainingTask(Runnable task) { super.cancelRemainingTask(task); 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 458af46ae743..729be479e0cf 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. @@ -19,7 +19,6 @@ import java.time.Clock; import java.time.Duration; import java.time.Instant; -import java.util.Map; import java.util.concurrent.Callable; import java.util.concurrent.Delayed; import java.util.concurrent.ExecutionException; @@ -36,19 +35,17 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskRejectedException; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.support.TaskUtils; import org.springframework.util.Assert; -import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ErrorHandler; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; /** * A standard implementation of Spring's {@link TaskScheduler} interface, wrapping @@ -74,9 +71,9 @@ * @see ThreadPoolTaskExecutor * @see SimpleAsyncTaskScheduler */ -@SuppressWarnings({"serial", "deprecation", "removal"}) +@SuppressWarnings({"serial", "deprecation"}) public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport - implements AsyncListenableTaskExecutor, SchedulingTaskExecutor, TaskScheduler { + implements AsyncTaskExecutor, SchedulingTaskExecutor, TaskScheduler { private static final TimeUnit NANO = TimeUnit.NANOSECONDS; @@ -89,20 +86,13 @@ public class ThreadPoolTaskScheduler extends ExecutorConfigurationSupport private volatile boolean executeExistingDelayedTasksAfterShutdownPolicy = true; - @Nullable - private TaskDecorator taskDecorator; + private @Nullable TaskDecorator taskDecorator; - @Nullable - private volatile ErrorHandler errorHandler; + private volatile @Nullable ErrorHandler errorHandler; private Clock clock = Clock.systemDefaultZone(); - @Nullable - private ScheduledExecutorService scheduledExecutor; - - // Underlying ScheduledFutureTask to user-level ListenableFuture handle, if any - private final Map> listenableFutureMap = - new ConcurrentReferenceHashMap<>(16, ConcurrentReferenceHashMap.ReferenceType.WEAK); + private @Nullable ScheduledExecutorService scheduledExecutor; /** @@ -309,10 +299,9 @@ public int getActiveCount() { /** * 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. @@ -357,55 +346,11 @@ public Future submit(Callable task) { } } - @Override - public ListenableFuture submitListenable(Runnable task) { - ExecutorService executor = getScheduledExecutor(); - try { - ListenableFutureTask listenableFuture = new ListenableFutureTask<>(task, null); - executeAndTrack(executor, listenableFuture); - return listenableFuture; - } - catch (RejectedExecutionException ex) { - throw new TaskRejectedException(executor, task, ex); - } - } - - @Override - public ListenableFuture submitListenable(Callable task) { - ExecutorService executor = getScheduledExecutor(); - try { - ListenableFutureTask listenableFuture = new ListenableFutureTask<>(task); - executeAndTrack(executor, listenableFuture); - return listenableFuture; - } - catch (RejectedExecutionException ex) { - throw new TaskRejectedException(executor, task, ex); - } - } - - private void executeAndTrack(ExecutorService executor, ListenableFutureTask listenableFuture) { - Future scheduledFuture = executor.submit(errorHandlingTask(listenableFuture, false)); - this.listenableFutureMap.put(scheduledFuture, listenableFuture); - listenableFuture.addCallback(result -> this.listenableFutureMap.remove(scheduledFuture), - ex -> this.listenableFutureMap.remove(scheduledFuture)); - } - - @Override - protected void cancelRemainingTask(Runnable task) { - super.cancelRemainingTask(task); - // Cancel associated user-level ListenableFuture handle as well - ListenableFuture listenableFuture = this.listenableFutureMap.get(task); - if (listenableFuture != null) { - listenableFuture.cancel(true); - } - } - // TaskScheduler implementation @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { ScheduledExecutorService executor = getScheduledExecutor(); try { ErrorHandler errorHandler = this.errorHandler; diff --git a/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java index 7caa0796d908..435f8199d228 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java +++ b/spring-context/src/main/java/org/springframework/scheduling/concurrent/package-info.java @@ -5,9 +5,7 @@ * context. Provides support for the native {@code java.util.concurrent} * interfaces as well as the Spring {@code TaskExecutor} mechanism. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling.concurrent; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java index 8f09ae665a44..c10e3d64ed50 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/AnnotationDrivenBeanDefinitionParser.java @@ -16,6 +16,7 @@ package org.springframework.scheduling.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.aop.config.AopNamespaceUtils; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -47,8 +47,7 @@ public class AnnotationDrivenBeanDefinitionParser implements BeanDefinitionParse @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { Object source = parserContext.extractSource(element); // Register component for the surrounding element. diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java index 4f0265237e31..14062c14b5a6 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTask.java @@ -20,7 +20,7 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A representation of a scheduled task at runtime, @@ -38,8 +38,7 @@ public final class ScheduledTask { private final Task task; - @Nullable - volatile ScheduledFuture future; + volatile @Nullable ScheduledFuture future; ScheduledTask(Task task) { @@ -84,8 +83,7 @@ public void cancel(boolean mayInterruptIfRunning) { * if the task has been cancelled or no new execution is scheduled. * @since 6.2 */ - @Nullable - public Instant nextExecution() { + public @Nullable Instant nextExecution() { ScheduledFuture future = this.future; if (future != null && !future.isCancelled()) { long delay = future.getDelay(TimeUnit.MILLISECONDS); diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java index 700c0e9e27e2..49f5d455d1db 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/ScheduledTaskRegistrar.java @@ -29,10 +29,10 @@ import java.util.concurrent.ScheduledExecutorService; import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.concurrent.ConcurrentTaskScheduler; @@ -74,29 +74,21 @@ public class ScheduledTaskRegistrar implements ScheduledTaskHolder, Initializing public static final String CRON_DISABLED = "-"; - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; - @Nullable - private ScheduledExecutorService localExecutor; + private @Nullable ScheduledExecutorService localExecutor; - @Nullable - private ObservationRegistry observationRegistry; + private @Nullable ObservationRegistry observationRegistry; - @Nullable - private List triggerTasks; + private @Nullable List triggerTasks; - @Nullable - private List cronTasks; + private @Nullable List cronTasks; - @Nullable - private List fixedRateTasks; + private @Nullable List fixedRateTasks; - @Nullable - private List fixedDelayTasks; + private @Nullable List fixedDelayTasks; - @Nullable - private List oneTimeTasks; + private @Nullable List oneTimeTasks; private final Map unresolvedTasks = new HashMap<>(16); @@ -134,8 +126,7 @@ else if (scheduler instanceof ScheduledExecutorService ses) { /** * Return the {@link TaskScheduler} instance for this registrar (may be {@code null}). */ - @Nullable - public TaskScheduler getScheduler() { + public @Nullable TaskScheduler getScheduler() { return this.taskScheduler; } @@ -151,8 +142,7 @@ public void setObservationRegistry(@Nullable ObservationRegistry observationRegi * Return the {@link ObservationRegistry} for this registrar. * @since 6.1 */ - @Nullable - public ObservationRegistry getObservationRegistry() { + public @Nullable ObservationRegistry getObservationRegistry() { return this.observationRegistry; } @@ -485,8 +475,7 @@ private void addScheduledTask(@Nullable ScheduledTask task) { * @return a handle to the scheduled task, allowing to cancel it * @since 4.3 */ - @Nullable - public ScheduledTask scheduleTriggerTask(TriggerTask task) { + public @Nullable ScheduledTask scheduleTriggerTask(TriggerTask task) { ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); boolean newTask = false; if (scheduledTask == null) { @@ -510,8 +499,7 @@ public ScheduledTask scheduleTriggerTask(TriggerTask task) { * (or {@code null} if processing a previously registered task) * @since 4.3 */ - @Nullable - public ScheduledTask scheduleCronTask(CronTask task) { + public @Nullable ScheduledTask scheduleCronTask(CronTask task) { ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); boolean newTask = false; if (scheduledTask == null) { @@ -535,8 +523,7 @@ public ScheduledTask scheduleCronTask(CronTask task) { * (or {@code null} if processing a previously registered task) * @since 5.0.2 */ - @Nullable - public ScheduledTask scheduleFixedRateTask(FixedRateTask task) { + public @Nullable ScheduledTask scheduleFixedRateTask(FixedRateTask task) { ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); boolean newTask = false; if (scheduledTask == null) { @@ -569,8 +556,7 @@ public ScheduledTask scheduleFixedRateTask(FixedRateTask task) { * (or {@code null} if processing a previously registered task) * @since 5.0.2 */ - @Nullable - public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) { + public @Nullable ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) { ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); boolean newTask = false; if (scheduledTask == null) { @@ -603,8 +589,7 @@ public ScheduledTask scheduleFixedDelayTask(FixedDelayTask task) { * (or {@code null} if processing a previously registered task) * @since 6.1 */ - @Nullable - public ScheduledTask scheduleOneTimeTask(OneTimeTask task) { + public @Nullable ScheduledTask scheduleOneTimeTask(OneTimeTask task) { ScheduledTask scheduledTask = this.unresolvedTasks.remove(task); boolean newTask = false; if (scheduledTask == null) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java index ae14768a7d85..ef9c9042d5dd 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/Task.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/Task.java @@ -18,7 +18,8 @@ import java.time.Instant; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.util.Assert; @@ -99,9 +100,8 @@ public boolean isLongLived() { return SchedulingAwareRunnable.super.isLongLived(); } - @Nullable @Override - public String getQualifier() { + public @Nullable String getQualifier() { if (this.runnable instanceof SchedulingAwareRunnable sar) { return sar.getQualifier(); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java index cbb6e1ec4cb8..b049296dc08f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutionOutcome.java @@ -18,7 +18,8 @@ import java.time.Instant; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java index b065156dd0ee..6566263f6200 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskExecutorFactoryBean.java @@ -18,12 +18,13 @@ import java.util.concurrent.RejectedExecutionHandler; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.task.TaskExecutor; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.StringUtils; @@ -38,23 +39,17 @@ public class TaskExecutorFactoryBean implements FactoryBean, BeanNameAware, InitializingBean, DisposableBean { - @Nullable - private String poolSize; + private @Nullable String poolSize; - @Nullable - private Integer queueCapacity; + private @Nullable Integer queueCapacity; - @Nullable - private RejectedExecutionHandler rejectedExecutionHandler; + private @Nullable RejectedExecutionHandler rejectedExecutionHandler; - @Nullable - private Integer keepAliveSeconds; + private @Nullable Integer keepAliveSeconds; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private ThreadPoolTaskExecutor target; + private @Nullable ThreadPoolTaskExecutor target; public void setPoolSize(String poolSize) { @@ -144,8 +139,7 @@ private void determinePoolSizeRange(ThreadPoolTaskExecutor executor) { @Override - @Nullable - public TaskExecutor getObject() { + public @Nullable TaskExecutor getObject() { return this.target; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java b/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java index 21898b50efa2..0a4080d2d03b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/TaskSchedulerRouter.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -38,7 +39,6 @@ import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.config.EmbeddedValueResolver; import org.springframework.beans.factory.config.NamedBeanHolder; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; @@ -69,19 +69,15 @@ public class TaskSchedulerRouter implements TaskScheduler, BeanNameAware, BeanFa protected static final Log logger = LogFactory.getLog(TaskSchedulerRouter.class); - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; private final Supplier defaultScheduler = SingletonSupplier.of(this::determineDefaultScheduler); - @Nullable - private volatile ScheduledExecutorService localExecutor; + private volatile @Nullable ScheduledExecutorService localExecutor; /** @@ -106,8 +102,7 @@ public void setBeanFactory(@Nullable BeanFactory beanFactory) { @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { return determineTargetScheduler(task).schedule(task, trigger); } @@ -150,8 +145,7 @@ protected TaskScheduler determineTargetScheduler(Runnable task) { } } - @Nullable - protected String determineQualifier(Runnable task) { + protected @Nullable String determineQualifier(Runnable task) { return (task instanceof SchedulingAwareRunnable sar ? sar.getQualifier() : null); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java index 3ddfc3ca96ec..962ffb54bfca 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java +++ b/spring-context/src/main/java/org/springframework/scheduling/config/package-info.java @@ -2,9 +2,7 @@ * Support package for declarative scheduling configuration, * with XML schema being the primary configuration format. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scheduling/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/package-info.java index 8880b4ba376f..950be2506968 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/package-info.java +++ b/spring-context/src/main/java/org/springframework/scheduling/package-info.java @@ -2,9 +2,7 @@ * General exceptions for Spring's scheduling support, * independent of any specific scheduling system. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java index 3ef274e63c5c..5b662d3bf94d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/BitsCronField.java @@ -20,7 +20,8 @@ import java.time.temporal.Temporal; import java.time.temporal.ValueRange; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -170,9 +171,8 @@ private static ValueRange parseRange(String value, Type type) { } - @Nullable @Override - public > T nextOrSame(T temporal) { + public > @Nullable T nextOrSame(T temporal) { int current = type().get(temporal); int next = nextSetBit(current); if (next == -1) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CompositeCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CompositeCronField.java index c69fdb12ff7a..856058189575 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CompositeCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CompositeCronField.java @@ -18,7 +18,8 @@ import java.time.temporal.Temporal; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -56,9 +57,8 @@ public static CronField compose(CronField[] fields, Type type, String value) { } - @Nullable @Override - public > T nextOrSame(T temporal) { + public > @Nullable T nextOrSame(T temporal) { T result = null; for (CronField field : this.fields) { T candidate = field.nextOrSame(temporal); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java index 47b5db1a2c18..bdb5249ea3c5 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronExpression.java @@ -20,7 +20,8 @@ import java.time.temporal.Temporal; import java.util.Arrays; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -236,14 +237,12 @@ private static String resolveMacros(String expression) { * @return the next temporal that matches this expression, or {@code null} * if no such temporal can be found */ - @Nullable - public > T next(T temporal) { + public > @Nullable T next(T temporal) { return nextOrSame(ChronoUnit.NANOS.addTo(temporal, 1)); } - @Nullable - private > T nextOrSame(T temporal) { + private > @Nullable T nextOrSame(T temporal) { for (int i = 0; i < MAX_ATTEMPTS; i++) { T result = nextOrSameInternal(temporal); if (result == null || result.equals(temporal)) { @@ -254,8 +253,7 @@ private > T nextOrSame(T temporal) { return null; } - @Nullable - private > T nextOrSameInternal(T temporal) { + private > @Nullable T nextOrSameInternal(T temporal) { for (CronField field : this.fields) { temporal = field.nextOrSame(temporal); if (temporal == null) { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java index e1702c9a548b..31161c85b30d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronField.java @@ -24,7 +24,8 @@ import java.util.Locale; import java.util.function.BiFunction; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -159,8 +160,7 @@ private static String replaceOrdinals(String value, String[] list) { * @param temporal the seed value * @return the next or same temporal matching the pattern */ - @Nullable - public abstract > T nextOrSame(T temporal); + public abstract > @Nullable T nextOrSame(T temporal); protected Type type() { diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java index dc8c9a4ab527..8aab327bf5fd 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/CronTrigger.java @@ -21,7 +21,8 @@ import java.time.ZonedDateTime; import java.util.TimeZone; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.util.Assert; @@ -45,8 +46,7 @@ public class CronTrigger implements Trigger { private final CronExpression expression; - @Nullable - private final ZoneId zoneId; + private final @Nullable ZoneId zoneId; /** @@ -112,8 +112,7 @@ public String getExpression() { * previous execution; therefore, overlapping executions won't occur. */ @Override - @Nullable - public Instant nextExecution(TriggerContext triggerContext) { + public @Nullable Instant nextExecution(TriggerContext triggerContext) { Instant timestamp = determineLatestTimestamp(triggerContext); ZoneId zone = (this.zoneId != null ? this.zoneId : triggerContext.getClock().getZone()); ZonedDateTime zonedTimestamp = ZonedDateTime.ofInstant(timestamp, zone); diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java index 9b3c891070f8..511531069b7f 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/DefaultScheduledTaskObservationConvention.java @@ -48,8 +48,8 @@ public String getName() { @Override public String getContextualName(ScheduledTaskObservationContext context) { - return "task " + StringUtils.uncapitalize(context.getTargetClass().getSimpleName()) - + "." + context.getMethod().getName(); + return "task " + StringUtils.uncapitalize(context.getTargetClass().getSimpleName()) + + "." + context.getMethod().getName(); } @Override diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java index 8b354ebec3d7..067d29540042 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/MethodInvokingRunnable.java @@ -20,11 +20,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.support.ArgumentConvertingMethodInvoker; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -43,8 +43,7 @@ public class MethodInvokingRunnable extends ArgumentConvertingMethodInvoker protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); @Override diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java b/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java index c2733dd86ea7..cf5680de944c 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/NoOpTaskScheduler.java @@ -23,7 +23,8 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.Trigger; @@ -39,8 +40,7 @@ public class NoOpTaskScheduler implements TaskScheduler { @Override - @Nullable - public ScheduledFuture schedule(Runnable task, Trigger trigger) { + public @Nullable ScheduledFuture schedule(Runnable task, Trigger trigger) { Instant nextExecution = trigger.nextExecution(new SimpleTriggerContext(getClock())); return (nextExecution != null ? new NoOpScheduledFuture<>() : null); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java b/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java index 71096a893460..fa075a276293 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/PeriodicTrigger.java @@ -21,7 +21,8 @@ import java.time.temporal.ChronoUnit; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.util.Assert; @@ -51,11 +52,9 @@ public class PeriodicTrigger implements Trigger { private final Duration period; - @Nullable - private final ChronoUnit chronoUnit; + private final @Nullable ChronoUnit chronoUnit; - @Nullable - private volatile Duration initialDelay; + private volatile @Nullable Duration initialDelay; private volatile boolean fixedRate; @@ -197,8 +196,7 @@ public long getInitialDelay() { * Return the initial delay, or {@code null} if none. * @since 6.0 */ - @Nullable - public Duration getInitialDelayDuration() { + public @Nullable Duration getInitialDelayDuration() { return this.initialDelay; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java index b49b49357c03..1d51d485ae35 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/QuartzCronField.java @@ -24,7 +24,8 @@ import java.time.temporal.TemporalAdjuster; import java.time.temporal.TemporalAdjusters; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -346,8 +347,7 @@ private static Temporal rollbackToMidnight(Temporal current, Temporal result) { @Override - @Nullable - public > T nextOrSame(T temporal) { + public > @Nullable T nextOrSame(T temporal) { T result = adjust(temporal); if (result != null) { if (result.compareTo(temporal) < 0) { @@ -362,9 +362,8 @@ public > T nextOrSame(T temporal) { return result; } - @Nullable @SuppressWarnings("unchecked") - private > T adjust(T temporal) { + private > @Nullable T adjust(T temporal) { return (T) this.adjuster.adjustInto(temporal); } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java index 87412218f902..57312e3c416d 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/ScheduledMethodRunnable.java @@ -23,8 +23,8 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.util.ReflectionUtils; @@ -47,8 +47,7 @@ public class ScheduledMethodRunnable implements SchedulingAwareRunnable { private final Method method; - @Nullable - private final String qualifier; + private final @Nullable String qualifier; private final Supplier observationRegistrySupplier; @@ -109,8 +108,7 @@ public Method getMethod() { } @Override - @Nullable - public String getQualifier() { + public @Nullable String getQualifier() { return this.qualifier; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java b/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java index 52088fb41dde..519a19aa86f7 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/SimpleTriggerContext.java @@ -20,7 +20,8 @@ import java.time.Instant; import java.util.Date; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.TriggerContext; /** @@ -33,14 +34,11 @@ public class SimpleTriggerContext implements TriggerContext { private final Clock clock; - @Nullable - private volatile Instant lastScheduledExecution; + private volatile @Nullable Instant lastScheduledExecution; - @Nullable - private volatile Instant lastActualExecution; + private volatile @Nullable Instant lastActualExecution; - @Nullable - private volatile Instant lastCompletion; + private volatile @Nullable Instant lastCompletion; /** @@ -66,8 +64,7 @@ public SimpleTriggerContext(@Nullable Date lastScheduledExecutionTime, @Nullable this(toInstant(lastScheduledExecutionTime), toInstant(lastActualExecutionTime), toInstant(lastCompletionTime)); } - @Nullable - private static Instant toInstant(@Nullable Date date) { + private static @Nullable Instant toInstant(@Nullable Date date) { return (date != null ? date.toInstant() : null); } @@ -134,20 +131,17 @@ public Clock getClock() { } @Override - @Nullable - public Instant lastScheduledExecution() { + public @Nullable Instant lastScheduledExecution() { return this.lastScheduledExecution; } @Override - @Nullable - public Instant lastActualExecution() { + public @Nullable Instant lastActualExecution() { return this.lastActualExecution; } @Override - @Nullable - public Instant lastCompletion() { + public @Nullable Instant lastCompletion() { return this.lastCompletion; } diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java b/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java index 7a286639e38d..3d8a068fe68b 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/TaskUtils.java @@ -20,8 +20,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ErrorHandler; import org.springframework.util.ReflectionUtils; diff --git a/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java b/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java index 228c69c6a956..485d12506043 100644 --- a/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/scheduling/support/package-info.java @@ -2,9 +2,7 @@ * Generic support classes for scheduling. * Provides a Runnable adapter for Spring's MethodInvoker. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scheduling.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java b/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java index 25e4b0815105..91e8107de23e 100644 --- a/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptCompilationException.java @@ -16,8 +16,9 @@ package org.springframework.scripting; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * Exception to be thrown on script compilation failure. @@ -28,8 +29,7 @@ @SuppressWarnings("serial") public class ScriptCompilationException extends NestedRuntimeException { - @Nullable - private final ScriptSource scriptSource; + private final @Nullable ScriptSource scriptSource; /** @@ -88,8 +88,7 @@ public ScriptCompilationException(ScriptSource scriptSource, String msg, Throwab * Return the source for the offending script. * @return the source, or {@code null} if not available */ - @Nullable - public ScriptSource getScriptSource() { + public @Nullable ScriptSource getScriptSource() { return this.scriptSource; } diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java index 762a2282e742..3fa401bcc805 100644 --- a/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptEvaluator.java @@ -18,7 +18,7 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Spring's strategy interface for evaluating a script. @@ -40,8 +40,7 @@ public interface ScriptEvaluator { * @throws ScriptCompilationException if the evaluator failed to read, * compile or evaluate the script */ - @Nullable - Object evaluate(ScriptSource script) throws ScriptCompilationException; + @Nullable Object evaluate(ScriptSource script) throws ScriptCompilationException; /** * Evaluate the given script with the given arguments. @@ -52,7 +51,6 @@ public interface ScriptEvaluator { * @throws ScriptCompilationException if the evaluator failed to read, * compile or evaluate the script */ - @Nullable - Object evaluate(ScriptSource script, @Nullable Map arguments) throws ScriptCompilationException; + @Nullable Object evaluate(ScriptSource script, @Nullable Map arguments) throws ScriptCompilationException; } diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java index 5c535c36f04c..fb60a7ce317f 100644 --- a/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptFactory.java @@ -18,7 +18,7 @@ import java.io.IOException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Script definition interface, encapsulating the configuration @@ -51,8 +51,7 @@ public interface ScriptFactory { * its Java interfaces (such as in the case of Groovy). * @return the interfaces for the script */ - @Nullable - Class[] getScriptInterfaces(); + Class @Nullable [] getScriptInterfaces(); /** * Return whether the script requires a config interface to be @@ -78,8 +77,7 @@ public interface ScriptFactory { * @throws IOException if script retrieval failed * @throws ScriptCompilationException if script compilation failed */ - @Nullable - Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + @Nullable Object getScriptedObject(ScriptSource scriptSource, Class @Nullable ... actualInterfaces) throws IOException, ScriptCompilationException; /** @@ -95,8 +93,7 @@ Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actual * @throws ScriptCompilationException if script compilation failed * @since 2.0.3 */ - @Nullable - Class getScriptedObjectType(ScriptSource scriptSource) + @Nullable Class getScriptedObjectType(ScriptSource scriptSource) throws IOException, ScriptCompilationException; /** diff --git a/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java b/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java index 87e695470402..75515cc0d1b0 100644 --- a/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java +++ b/spring-context/src/main/java/org/springframework/scripting/ScriptSource.java @@ -18,7 +18,7 @@ import java.io.IOException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that defines the source of a script. @@ -49,7 +49,6 @@ public interface ScriptSource { * Determine a class name for the underlying script. * @return the suggested class name, or {@code null} if none available */ - @Nullable - String suggestedClassName(); + @Nullable String suggestedClassName(); } diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java index 07697f0d0890..b6aef2c71f4b 100644 --- a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptEvaluator.java @@ -22,9 +22,9 @@ import bsh.EvalError; import bsh.Interpreter; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptEvaluator; import org.springframework.scripting.ScriptSource; @@ -38,8 +38,7 @@ */ public class BshScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware { - @Nullable - private ClassLoader classLoader; + private @Nullable ClassLoader classLoader; /** @@ -64,14 +63,12 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override - @Nullable - public Object evaluate(ScriptSource script) { + public @Nullable Object evaluate(ScriptSource script) { return evaluate(script, null); } @Override - @Nullable - public Object evaluate(ScriptSource script, @Nullable Map arguments) { + public @Nullable Object evaluate(ScriptSource script, @Nullable Map arguments) { try { Interpreter interpreter = new Interpreter(); interpreter.setClassLoader(this.classLoader); diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java index 235593df9ed8..4d355ffa3094 100644 --- a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptFactory.java @@ -19,9 +19,9 @@ import java.io.IOException; import bsh.EvalError; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptFactory; import org.springframework.scripting.ScriptSource; @@ -47,14 +47,11 @@ public class BshScriptFactory implements ScriptFactory, BeanClassLoaderAware { private final String scriptSourceLocator; - @Nullable - private final Class[] scriptInterfaces; + private final Class @Nullable [] scriptInterfaces; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private Class scriptClass; + private @Nullable Class scriptClass; private final Object scriptClassMonitor = new Object(); @@ -85,7 +82,7 @@ public BshScriptFactory(String scriptSourceLocator) { * @param scriptInterfaces the Java interfaces that the scripted object * is supposed to implement (may be {@code null}) */ - public BshScriptFactory(String scriptSourceLocator, @Nullable Class... scriptInterfaces) { + public BshScriptFactory(String scriptSourceLocator, Class @Nullable ... scriptInterfaces) { Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); this.scriptSourceLocator = scriptSourceLocator; this.scriptInterfaces = scriptInterfaces; @@ -104,8 +101,7 @@ public String getScriptSourceLocator() { } @Override - @Nullable - public Class[] getScriptInterfaces() { + public Class @Nullable [] getScriptInterfaces() { return this.scriptInterfaces; } @@ -122,8 +118,7 @@ public boolean requiresConfigInterface() { * @see BshScriptUtils#createBshObject(String, Class[], ClassLoader) */ @Override - @Nullable - public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + public @Nullable Object getScriptedObject(ScriptSource scriptSource, Class @Nullable ... actualInterfaces) throws IOException, ScriptCompilationException { Class clazz; @@ -181,8 +176,7 @@ public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... } @Override - @Nullable - public Class getScriptedObjectType(ScriptSource scriptSource) + public @Nullable Class getScriptedObjectType(ScriptSource scriptSource) throws IOException, ScriptCompilationException { synchronized (this.scriptClassMonitor) { diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java index 8469e4884cac..308ea1950fb5 100644 --- a/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/BshScriptUtils.java @@ -24,9 +24,9 @@ import bsh.Interpreter; import bsh.Primitive; import bsh.XThis; +import org.jspecify.annotations.Nullable; import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -68,7 +68,7 @@ public static Object createBshObject(String scriptSource) throws EvalError { * @throws EvalError in case of BeanShell parsing failure * @see #createBshObject(String, Class[], ClassLoader) */ - public static Object createBshObject(String scriptSource, @Nullable Class... scriptInterfaces) throws EvalError { + public static Object createBshObject(String scriptSource, Class @Nullable ... scriptInterfaces) throws EvalError { return createBshObject(scriptSource, scriptInterfaces, ClassUtils.getDefaultClassLoader()); } @@ -86,7 +86,7 @@ public static Object createBshObject(String scriptSource, @Nullable Class... * @return the scripted Java object * @throws EvalError in case of BeanShell parsing failure */ - public static Object createBshObject(String scriptSource, @Nullable Class[] scriptInterfaces, @Nullable ClassLoader classLoader) + public static Object createBshObject(String scriptSource, Class @Nullable [] scriptInterfaces, @Nullable ClassLoader classLoader) throws EvalError { Object result = evaluateBshScript(scriptSource, scriptInterfaces, classLoader); @@ -114,8 +114,7 @@ public static Object createBshObject(String scriptSource, @Nullable Class[] s * @return the scripted Java class, or {@code null} if none could be determined * @throws EvalError in case of BeanShell parsing failure */ - @Nullable - static Class determineBshObjectType(String scriptSource, @Nullable ClassLoader classLoader) throws EvalError { + static @Nullable Class determineBshObjectType(String scriptSource, @Nullable ClassLoader classLoader) throws EvalError { Assert.hasText(scriptSource, "Script source must not be empty"); Interpreter interpreter = new Interpreter(); if (classLoader != null) { @@ -149,7 +148,7 @@ else if (result != null) { * @throws EvalError in case of BeanShell parsing failure */ static Object evaluateBshScript( - String scriptSource, @Nullable Class[] scriptInterfaces, @Nullable ClassLoader classLoader) + String scriptSource, Class @Nullable [] scriptInterfaces, @Nullable ClassLoader classLoader) throws EvalError { Assert.hasText(scriptSource, "Script source must not be empty"); @@ -183,8 +182,7 @@ public BshObjectInvocationHandler(XThis xt) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (ReflectionUtils.isEqualsMethod(method)) { return (isProxyForSameBshObject(args[0])); } diff --git a/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java b/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java index eb70e4afb8d1..d5e2fb0d2776 100644 --- a/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java +++ b/spring-context/src/main/java/org/springframework/scripting/bsh/package-info.java @@ -4,9 +4,7 @@ * (and BeanShell2) * into Spring's scripting infrastructure. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scripting.bsh; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java index 06d6cd7727fb..47c0a447f930 100644 --- a/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/scripting/config/ScriptBeanDefinitionParser.java @@ -18,6 +18,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.ConstructorArgumentValues; @@ -29,7 +30,6 @@ import org.springframework.beans.factory.xml.BeanDefinitionParserDelegate; import org.springframework.beans.factory.xml.ParserContext; import org.springframework.beans.factory.xml.XmlReaderContext; -import org.springframework.lang.Nullable; import org.springframework.scripting.support.ScriptFactoryPostProcessor; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -104,8 +104,7 @@ public ScriptBeanDefinitionParser(String scriptFactoryClassName) { */ @Override @SuppressWarnings("deprecation") - @Nullable - protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { + protected @Nullable AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) { // Engine attribute only supported for String engine = element.getAttribute(ENGINE_ATTRIBUTE); @@ -215,8 +214,7 @@ else if (beanDefinitionDefaults.getDestroyMethodName() != null) { * the '{@code inline-script}' element. Logs and {@link XmlReaderContext#error} and * returns {@code null} if neither or both of these values are specified. */ - @Nullable - private String resolveScriptSource(Element element, XmlReaderContext readerContext) { + private @Nullable String resolveScriptSource(Element element, XmlReaderContext readerContext) { boolean hasScriptSource = element.hasAttribute(SCRIPT_SOURCE_ATTRIBUTE); List elements = DomUtils.getChildElementsByTagName(element, INLINE_SCRIPT_ELEMENT); if (hasScriptSource && !elements.isEmpty()) { diff --git a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java index f4068e09cbb0..60b3ec1b993a 100644 --- a/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java +++ b/spring-context/src/main/java/org/springframework/scripting/config/ScriptingDefaultsParser.java @@ -16,13 +16,13 @@ package org.springframework.scripting.config; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.TypedStringValue; import org.springframework.beans.factory.xml.BeanDefinitionParser; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -39,8 +39,7 @@ class ScriptingDefaultsParser implements BeanDefinitionParser { @Override - @Nullable - public BeanDefinition parse(Element element, ParserContext parserContext) { + public @Nullable BeanDefinition parse(Element element, ParserContext parserContext) { BeanDefinition bd = LangNamespaceUtils.registerScriptFactoryPostProcessorIfNecessary(parserContext.getRegistry()); String refreshCheckDelay = element.getAttribute(REFRESH_CHECK_DELAY_ATTRIBUTE); diff --git a/spring-context/src/main/java/org/springframework/scripting/config/package-info.java b/spring-context/src/main/java/org/springframework/scripting/config/package-info.java index 0d2c295d5b93..e10b2dde004f 100644 --- a/spring-context/src/main/java/org/springframework/scripting/config/package-info.java +++ b/spring-context/src/main/java/org/springframework/scripting/config/package-info.java @@ -2,9 +2,7 @@ * Support package for Spring's dynamic language machinery, * with XML schema being the primary configuration format. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scripting.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java index 21a34f10a35f..e63945ee87bf 100644 --- a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptEvaluator.java @@ -24,9 +24,9 @@ import groovy.lang.GroovyShell; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptEvaluator; import org.springframework.scripting.ScriptSource; @@ -41,8 +41,7 @@ */ public class GroovyScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware { - @Nullable - private ClassLoader classLoader; + private @Nullable ClassLoader classLoader; private CompilerConfiguration compilerConfiguration = new CompilerConfiguration(); @@ -98,14 +97,12 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override - @Nullable - public Object evaluate(ScriptSource script) { + public @Nullable Object evaluate(ScriptSource script) { return evaluate(script, null); } @Override - @Nullable - public Object evaluate(ScriptSource script, @Nullable Map arguments) { + public @Nullable Object evaluate(ScriptSource script, @Nullable Map arguments) { GroovyShell groovyShell = new GroovyShell( this.classLoader, new Binding(arguments), this.compilerConfiguration); try { diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java index 216518606829..d3faceca8982 100644 --- a/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/GroovyScriptFactory.java @@ -26,12 +26,12 @@ import org.codehaus.groovy.control.CompilationFailedException; import org.codehaus.groovy.control.CompilerConfiguration; import org.codehaus.groovy.control.customizers.CompilationCustomizer; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptFactory; import org.springframework.scripting.ScriptSource; @@ -61,23 +61,17 @@ public class GroovyScriptFactory implements ScriptFactory, BeanFactoryAware, Bea private final String scriptSourceLocator; - @Nullable - private GroovyObjectCustomizer groovyObjectCustomizer; + private @Nullable GroovyObjectCustomizer groovyObjectCustomizer; - @Nullable - private CompilerConfiguration compilerConfiguration; + private @Nullable CompilerConfiguration compilerConfiguration; - @Nullable - private GroovyClassLoader groovyClassLoader; + private @Nullable GroovyClassLoader groovyClassLoader; - @Nullable - private Class scriptClass; + private @Nullable Class scriptClass; - @Nullable - private Class scriptResultClass; + private @Nullable Class scriptResultClass; - @Nullable - private CachedResultHolder cachedResult; + private @Nullable CachedResultHolder cachedResult; private final Object scriptClassMonitor = new Object(); @@ -201,8 +195,7 @@ public String getScriptSourceLocator() { * @return {@code null} always */ @Override - @Nullable - public Class[] getScriptInterfaces() { + public Class @Nullable [] getScriptInterfaces() { return null; } @@ -221,8 +214,7 @@ public boolean requiresConfigInterface() { * @see groovy.lang.GroovyClassLoader */ @Override - @Nullable - public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + public @Nullable Object getScriptedObject(ScriptSource scriptSource, Class @Nullable ... actualInterfaces) throws IOException, ScriptCompilationException { synchronized (this.scriptClassMonitor) { @@ -265,8 +257,7 @@ public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... } @Override - @Nullable - public Class getScriptedObjectType(ScriptSource scriptSource) + public @Nullable Class getScriptedObjectType(ScriptSource scriptSource) throws IOException, ScriptCompilationException { synchronized (this.scriptClassMonitor) { @@ -314,8 +305,7 @@ public boolean requiresScriptedObjectRefresh(ScriptSource scriptSource) { * or the result of running the script instance) * @throws ScriptCompilationException in case of instantiation failure */ - @Nullable - protected Object executeScript(ScriptSource scriptSource, Class scriptClass) throws ScriptCompilationException { + protected @Nullable Object executeScript(ScriptSource scriptSource, Class scriptClass) throws ScriptCompilationException { try { GroovyObject groovyObj = (GroovyObject) ReflectionUtils.accessibleConstructor(scriptClass).newInstance(); @@ -363,8 +353,7 @@ public String toString() { */ private static class CachedResultHolder { - @Nullable - public final Object object; + public final @Nullable Object object; public CachedResultHolder(@Nullable Object object) { this.object = object; diff --git a/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java b/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java index 336139003789..fa250edaa085 100644 --- a/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java +++ b/spring-context/src/main/java/org/springframework/scripting/groovy/package-info.java @@ -3,9 +3,7 @@ * Groovy * into Spring's scripting infrastructure. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scripting.groovy; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scripting/package-info.java b/spring-context/src/main/java/org/springframework/scripting/package-info.java index 53a043f760d0..29d2f16d78c9 100644 --- a/spring-context/src/main/java/org/springframework/scripting/package-info.java +++ b/spring-context/src/main/java/org/springframework/scripting/package-info.java @@ -1,9 +1,7 @@ /** * Core interfaces for Spring's scripting support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scripting; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java b/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java index 0cdceadfe7f2..94860e0aa283 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/ResourceScriptSource.java @@ -22,10 +22,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptSource; import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; @@ -129,8 +129,7 @@ protected long retrieveLastModifiedTime() { } @Override - @Nullable - public String suggestedClassName() { + public @Nullable String suggestedClassName() { String filename = getResource().getFilename(); return (filename != null ? StringUtils.stripFilenameExtension(filename) : null); } diff --git a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java index ac1b678dfbf2..5d71cb0ebad6 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/ScriptFactoryPostProcessor.java @@ -21,6 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.TargetSource; import org.springframework.aop.framework.AopInfrastructureBean; @@ -51,7 +52,6 @@ import org.springframework.core.Ordered; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptFactory; import org.springframework.scripting.ScriptSource; import org.springframework.util.Assert; @@ -178,11 +178,9 @@ public class ScriptFactoryPostProcessor implements SmartInstantiationAwareBeanPo private boolean defaultProxyTargetClass = false; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private ConfigurableBeanFactory beanFactory; + private @Nullable ConfigurableBeanFactory beanFactory; private ResourceLoader resourceLoader = new DefaultResourceLoader(); @@ -248,8 +246,7 @@ public int getOrder() { @Override - @Nullable - public Class predictBeanType(Class beanClass, String beanName) { + public @Nullable Class predictBeanType(Class beanClass, String beanName) { // We only apply special treatment to ScriptFactory implementations here. if (!ScriptFactory.class.isAssignableFrom(beanClass)) { return null; @@ -304,8 +301,7 @@ public PropertyValues postProcessProperties(PropertyValues pvs, Object bean, Str } @Override - @Nullable - public Object postProcessBeforeInstantiation(Class beanClass, String beanName) { + public @Nullable Object postProcessBeforeInstantiation(Class beanClass, String beanName) { // We only apply special treatment to ScriptFactory implementations here. if (!ScriptFactory.class.isAssignableFrom(beanClass)) { return null; @@ -500,7 +496,7 @@ protected ScriptSource convertToScriptSource(String beanName, String scriptSourc * @see org.springframework.cglib.proxy.InterfaceMaker * @see org.springframework.beans.BeanUtils#findPropertyType */ - protected Class createConfigInterface(BeanDefinition bd, @Nullable Class[] interfaces) { + protected Class createConfigInterface(BeanDefinition bd, Class @Nullable [] interfaces) { InterfaceMaker maker = new InterfaceMaker(); PropertyValue[] pvs = bd.getPropertyValues().getPropertyValues(); for (PropertyValue pv : pvs) { @@ -546,7 +542,7 @@ protected Class createCompositeInterface(Class[] interfaces) { * @see org.springframework.scripting.ScriptFactory#getScriptedObject */ protected BeanDefinition createScriptedObjectBeanDefinition(BeanDefinition bd, String scriptFactoryBeanName, - ScriptSource scriptSource, @Nullable Class[] interfaces) { + ScriptSource scriptSource, Class @Nullable [] interfaces) { GenericBeanDefinition objectBd = new GenericBeanDefinition(bd); objectBd.setFactoryBeanName(scriptFactoryBeanName); @@ -565,7 +561,7 @@ protected BeanDefinition createScriptedObjectBeanDefinition(BeanDefinition bd, S * @return the generated proxy * @see RefreshableScriptTargetSource */ - protected Object createRefreshableProxy(TargetSource ts, @Nullable Class[] interfaces, boolean proxyTargetClass) { + protected Object createRefreshableProxy(TargetSource ts, Class @Nullable [] interfaces, boolean proxyTargetClass) { ProxyFactory proxyFactory = new ProxyFactory(); proxyFactory.setTargetSource(ts); ClassLoader classLoader = this.beanClassLoader; diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java index d907574ef2c7..4103d54695b3 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptEvaluator.java @@ -24,9 +24,10 @@ import javax.script.ScriptEngineManager; import javax.script.ScriptException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptEvaluator; import org.springframework.scripting.ScriptSource; @@ -44,14 +45,11 @@ */ public class StandardScriptEvaluator implements ScriptEvaluator, BeanClassLoaderAware { - @Nullable - private String engineName; + private @Nullable String engineName; - @Nullable - private volatile Bindings globalBindings; + private volatile @Nullable Bindings globalBindings; - @Nullable - private volatile ScriptEngineManager scriptEngineManager; + private volatile @Nullable ScriptEngineManager scriptEngineManager; /** @@ -132,14 +130,12 @@ public void setBeanClassLoader(ClassLoader classLoader) { @Override - @Nullable - public Object evaluate(ScriptSource script) { + public @Nullable Object evaluate(ScriptSource script) { return evaluate(script, null); } @Override - @Nullable - public Object evaluate(ScriptSource script, @Nullable Map argumentBindings) { + public @Nullable Object evaluate(ScriptSource script, @Nullable Map argumentBindings) { ScriptEngine engine = getScriptEngine(script); try { if (CollectionUtils.isEmpty(argumentBindings)) { diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java index a2e4c4f32f18..3373f26d4316 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StandardScriptFactory.java @@ -23,8 +23,9 @@ import javax.script.ScriptEngine; import javax.script.ScriptEngineManager; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.lang.Nullable; import org.springframework.scripting.ScriptCompilationException; import org.springframework.scripting.ScriptFactory; import org.springframework.scripting.ScriptSource; @@ -49,19 +50,15 @@ */ public class StandardScriptFactory implements ScriptFactory, BeanClassLoaderAware { - @Nullable - private final String scriptEngineName; + private final @Nullable String scriptEngineName; private final String scriptSourceLocator; - @Nullable - private final Class[] scriptInterfaces; + private final Class @Nullable [] scriptInterfaces; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); - @Nullable - private volatile ScriptEngine scriptEngine; + private volatile @Nullable ScriptEngine scriptEngine; /** @@ -105,7 +102,7 @@ public StandardScriptFactory(String scriptEngineName, String scriptSourceLocator * is supposed to implement */ public StandardScriptFactory( - @Nullable String scriptEngineName, String scriptSourceLocator, @Nullable Class... scriptInterfaces) { + @Nullable String scriptEngineName, String scriptSourceLocator, Class @Nullable ... scriptInterfaces) { Assert.hasText(scriptSourceLocator, "'scriptSourceLocator' must not be empty"); this.scriptEngineName = scriptEngineName; @@ -125,8 +122,7 @@ public String getScriptSourceLocator() { } @Override - @Nullable - public Class[] getScriptInterfaces() { + public Class @Nullable [] getScriptInterfaces() { return this.scriptInterfaces; } @@ -140,8 +136,7 @@ public boolean requiresConfigInterface() { * Load and parse the script via JSR-223's ScriptEngine. */ @Override - @Nullable - public Object getScriptedObject(ScriptSource scriptSource, @Nullable Class... actualInterfaces) + public @Nullable Object getScriptedObject(ScriptSource scriptSource, Class @Nullable ... actualInterfaces) throws IOException, ScriptCompilationException { Object script = evaluateScript(scriptSource); @@ -202,8 +197,7 @@ protected Object evaluateScript(ScriptSource scriptSource) { } } - @Nullable - protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { + protected @Nullable ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { ScriptEngineManager scriptEngineManager = new ScriptEngineManager(this.beanClassLoader); if (this.scriptEngineName != null) { @@ -226,8 +220,7 @@ protected ScriptEngine retrieveScriptEngine(ScriptSource scriptSource) { return null; } - @Nullable - protected Object adaptToInterfaces( + protected @Nullable Object adaptToInterfaces( @Nullable Object script, ScriptSource scriptSource, Class... actualInterfaces) { Class adaptedIfc; @@ -260,8 +253,7 @@ protected Object adaptToInterfaces( } @Override - @Nullable - public Class getScriptedObjectType(ScriptSource scriptSource) + public @Nullable Class getScriptedObjectType(ScriptSource scriptSource) throws IOException, ScriptCompilationException { return null; diff --git a/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java b/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java index c1163e9ece40..9b2fd1dc9e21 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/StaticScriptSource.java @@ -16,7 +16,8 @@ package org.springframework.scripting.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scripting.ScriptSource; import org.springframework.util.Assert; @@ -36,8 +37,7 @@ public class StaticScriptSource implements ScriptSource { private boolean modified; - @Nullable - private String className; + private @Nullable String className; /** @@ -82,8 +82,7 @@ public synchronized boolean isModified() { } @Override - @Nullable - public String suggestedClassName() { + public @Nullable String suggestedClassName() { return this.className; } diff --git a/spring-context/src/main/java/org/springframework/scripting/support/package-info.java b/spring-context/src/main/java/org/springframework/scripting/support/package-info.java index dc8b765754bb..c006546a2f2f 100644 --- a/spring-context/src/main/java/org/springframework/scripting/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/scripting/support/package-info.java @@ -3,9 +3,7 @@ * Provides a ScriptFactoryPostProcessor for turning ScriptFactory * definitions into scripted objects. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.scripting.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/stereotype/package-info.java b/spring-context/src/main/java/org/springframework/stereotype/package-info.java index 829c45e73e2c..9cb0299d7d32 100644 --- a/spring-context/src/main/java/org/springframework/stereotype/package-info.java +++ b/spring-context/src/main/java/org/springframework/stereotype/package-info.java @@ -4,9 +4,7 @@ * *

Intended for use by tools and aspects (making an ideal target for pointcuts). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.stereotype; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java index 25e3d4d7b052..4247bed6291e 100644 --- a/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java +++ b/spring-context/src/main/java/org/springframework/ui/ConcurrentModel.java @@ -20,8 +20,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.core.Conventions; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -66,8 +67,7 @@ public ConcurrentModel(Object attributeValue) { @Override - @Nullable - public Object put(String key, @Nullable Object value) { + public @Nullable Object put(String key, @Nullable Object value) { if (value != null) { return super.put(key, value); } @@ -169,8 +169,7 @@ public boolean containsAttribute(String attributeName) { } @Override - @Nullable - public Object getAttribute(String attributeName) { + public @Nullable Object getAttribute(String attributeName) { return get(attributeName); } diff --git a/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java b/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java index 5ec0a109e487..356e95964abd 100644 --- a/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java +++ b/spring-context/src/main/java/org/springframework/ui/ExtendedModelMap.java @@ -19,7 +19,7 @@ import java.util.Collection; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Subclass of {@link ModelMap} that implements the {@link Model} interface. diff --git a/spring-context/src/main/java/org/springframework/ui/Model.java b/spring-context/src/main/java/org/springframework/ui/Model.java index 83cd2ee8baee..e6ba37e63fb8 100644 --- a/spring-context/src/main/java/org/springframework/ui/Model.java +++ b/spring-context/src/main/java/org/springframework/ui/Model.java @@ -19,7 +19,7 @@ import java.util.Collection; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that defines a holder for model attributes. @@ -84,8 +84,7 @@ public interface Model { * @return the corresponding attribute value, or {@code null} if none * @since 5.2 */ - @Nullable - Object getAttribute(String attributeName); + @Nullable Object getAttribute(String attributeName); /** * Return the current set of model attributes as a Map. diff --git a/spring-context/src/main/java/org/springframework/ui/ModelMap.java b/spring-context/src/main/java/org/springframework/ui/ModelMap.java index 48c887666fb2..dac97a4ccd3a 100644 --- a/spring-context/src/main/java/org/springframework/ui/ModelMap.java +++ b/spring-context/src/main/java/org/springframework/ui/ModelMap.java @@ -20,8 +20,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.Conventions; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -150,8 +151,7 @@ public boolean containsAttribute(String attributeName) { * @return the corresponding attribute value, or {@code null} if none * @since 5.2 */ - @Nullable - public Object getAttribute(String attributeName) { + public @Nullable Object getAttribute(String attributeName) { return get(attributeName); } diff --git a/spring-context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java deleted file mode 100644 index ab52272b0df6..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/HierarchicalThemeSource.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.ui.context; - -import org.springframework.lang.Nullable; - -/** - * Sub-interface of ThemeSource to be implemented by objects that - * can resolve theme messages hierarchically. - * - * @author Jean-Pierre Pawlak - * @author Juergen Hoeller - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public interface HierarchicalThemeSource extends ThemeSource { - - /** - * Set the parent that will be used to try to resolve theme messages - * that this object can't resolve. - * @param parent the parent ThemeSource that will be used to - * resolve messages that this object can't resolve. - * May be {@code null}, in which case no further resolution is possible. - */ - void setParentThemeSource(@Nullable ThemeSource parent); - - /** - * Return the parent of this ThemeSource, or {@code null} if none. - */ - @Nullable - ThemeSource getParentThemeSource(); - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/Theme.java b/spring-context/src/main/java/org/springframework/ui/context/Theme.java deleted file mode 100644 index 2b079104149d..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/Theme.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.ui.context; - -import org.springframework.context.MessageSource; - -/** - * A Theme can resolve theme-specific messages, codes, file paths, etc. - * (e.g. CSS and image files in a web environment). - * The exposed {@link org.springframework.context.MessageSource} supports - * theme-specific parameterization and internationalization. - * - * @author Juergen Hoeller - * @since 17.06.2003 - * @see ThemeSource - * @see org.springframework.web.servlet.ThemeResolver - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public interface Theme { - - /** - * Return the name of the theme. - * @return the name of the theme (never {@code null}) - */ - String getName(); - - /** - * Return the specific MessageSource that resolves messages - * with respect to this theme. - * @return the theme-specific MessageSource (never {@code null}) - */ - MessageSource getMessageSource(); - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/ThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/ThemeSource.java deleted file mode 100644 index 15bf064d4687..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/ThemeSource.java +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.ui.context; - -import org.springframework.lang.Nullable; - -/** - * Interface to be implemented by objects that can resolve {@link Theme Themes}. - * This enables parameterization and internationalization of messages - * for a given 'theme'. - * - * @author Jean-Pierre Pawlak - * @author Juergen Hoeller - * @see Theme - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public interface ThemeSource { - - /** - * Return the Theme instance for the given theme name. - *

The returned Theme will resolve theme-specific messages, codes, - * file paths, etc (for example, CSS and image files in a web environment). - * @param themeName the name of the theme - * @return the corresponding Theme, or {@code null} if none defined. - * Note that, by convention, a ThemeSource should at least be able to - * return a default Theme for the default theme name "theme" but may also - * return default Themes for other theme names. - * @see org.springframework.web.servlet.theme.AbstractThemeResolver#ORIGINAL_DEFAULT_THEME_NAME - */ - @Nullable - Theme getTheme(String themeName); - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/package-info.java b/spring-context/src/main/java/org/springframework/ui/context/package-info.java deleted file mode 100644 index cedb3f0bba43..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Contains classes defining the application context subinterface - * for UI applications. The theme feature is added here. - */ -@NonNullApi -@NonNullFields -package org.springframework.ui.context; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java deleted file mode 100644 index 9e79100a5310..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/support/DelegatingThemeSource.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.ui.context.support; - -import org.springframework.lang.Nullable; -import org.springframework.ui.context.HierarchicalThemeSource; -import org.springframework.ui.context.Theme; -import org.springframework.ui.context.ThemeSource; - -/** - * Empty ThemeSource that delegates all calls to the parent ThemeSource. - * If no parent is available, it simply won't resolve any theme. - * - *

Used as placeholder by UiApplicationContextUtils, if a context doesn't - * define its own ThemeSource. Not intended for direct use in applications. - * - * @author Juergen Hoeller - * @since 1.2.4 - * @see UiApplicationContextUtils - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public class DelegatingThemeSource implements HierarchicalThemeSource { - - @Nullable - private ThemeSource parentThemeSource; - - - @Override - public void setParentThemeSource(@Nullable ThemeSource parentThemeSource) { - this.parentThemeSource = parentThemeSource; - } - - @Override - @Nullable - public ThemeSource getParentThemeSource() { - return this.parentThemeSource; - } - - - @Override - @Nullable - public Theme getTheme(String themeName) { - if (this.parentThemeSource != null) { - return this.parentThemeSource.getTheme(themeName); - } - else { - return null; - } - } - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java b/spring-context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java deleted file mode 100644 index f93bdf7e4da8..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/support/ResourceBundleThemeSource.java +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.ui.context.support; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.beans.factory.BeanClassLoaderAware; -import org.springframework.context.HierarchicalMessageSource; -import org.springframework.context.MessageSource; -import org.springframework.context.support.ResourceBundleMessageSource; -import org.springframework.lang.Nullable; -import org.springframework.ui.context.HierarchicalThemeSource; -import org.springframework.ui.context.Theme; -import org.springframework.ui.context.ThemeSource; - -/** - * {@link ThemeSource} implementation that looks up an individual - * {@link java.util.ResourceBundle} per theme. The theme name gets - * interpreted as ResourceBundle basename, supporting a common - * basename prefix for all themes. - * - * @author Jean-Pierre Pawlak - * @author Juergen Hoeller - * @see #setBasenamePrefix - * @see java.util.ResourceBundle - * @see org.springframework.context.support.ResourceBundleMessageSource - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public class ResourceBundleThemeSource implements HierarchicalThemeSource, BeanClassLoaderAware { - - protected final Log logger = LogFactory.getLog(getClass()); - - @Nullable - private ThemeSource parentThemeSource; - - private String basenamePrefix = ""; - - @Nullable - private String defaultEncoding; - - @Nullable - private Boolean fallbackToSystemLocale; - - @Nullable - private ClassLoader beanClassLoader; - - /** Map from theme name to Theme instance. */ - private final Map themeCache = new ConcurrentHashMap<>(); - - - @Override - public void setParentThemeSource(@Nullable ThemeSource parent) { - this.parentThemeSource = parent; - - // Update existing Theme objects. - // Usually there shouldn't be any at the time of this call. - synchronized (this.themeCache) { - for (Theme theme : this.themeCache.values()) { - initParent(theme); - } - } - } - - @Override - @Nullable - public ThemeSource getParentThemeSource() { - return this.parentThemeSource; - } - - /** - * Set the prefix that gets applied to the ResourceBundle basenames, - * i.e. the theme names. - * For example: basenamePrefix="test.", themeName="theme" → basename="test.theme". - *

Note that ResourceBundle names are effectively classpath locations: As a - * consequence, the JDK's standard ResourceBundle treats dots as package separators. - * This means that "test.theme" is effectively equivalent to "test/theme", - * just like it is for programmatic {@code java.util.ResourceBundle} usage. - * @see java.util.ResourceBundle#getBundle(String) - */ - public void setBasenamePrefix(@Nullable String basenamePrefix) { - this.basenamePrefix = (basenamePrefix != null ? basenamePrefix : ""); - } - - /** - * Set the default charset to use for parsing resource bundle files. - *

{@link ResourceBundleMessageSource}'s default is the - * {@code java.util.ResourceBundle} default encoding: ISO-8859-1. - * @since 4.2 - * @see ResourceBundleMessageSource#setDefaultEncoding - */ - public void setDefaultEncoding(@Nullable String defaultEncoding) { - this.defaultEncoding = defaultEncoding; - } - - /** - * Set whether to fall back to the system Locale if no files for a - * specific Locale have been found. - *

{@link ResourceBundleMessageSource}'s default is "true". - * @since 4.2 - * @see ResourceBundleMessageSource#setFallbackToSystemLocale - */ - public void setFallbackToSystemLocale(boolean fallbackToSystemLocale) { - this.fallbackToSystemLocale = fallbackToSystemLocale; - } - - @Override - public void setBeanClassLoader(@Nullable ClassLoader beanClassLoader) { - this.beanClassLoader = beanClassLoader; - } - - - /** - * This implementation returns a SimpleTheme instance, holding a - * ResourceBundle-based MessageSource whose basename corresponds to - * the given theme name (prefixed by the configured "basenamePrefix"). - *

SimpleTheme instances are cached per theme name. Use a reloadable - * MessageSource if themes should reflect changes to the underlying files. - * @see #setBasenamePrefix - * @see #createMessageSource - */ - @Override - @Nullable - public Theme getTheme(String themeName) { - Theme theme = this.themeCache.get(themeName); - if (theme == null) { - synchronized (this.themeCache) { - theme = this.themeCache.get(themeName); - if (theme == null) { - String basename = this.basenamePrefix + themeName; - MessageSource messageSource = createMessageSource(basename); - theme = new SimpleTheme(themeName, messageSource); - initParent(theme); - this.themeCache.put(themeName, theme); - if (logger.isDebugEnabled()) { - logger.debug("Theme created: name '" + themeName + "', basename [" + basename + "]"); - } - } - } - } - return theme; - } - - /** - * Create a MessageSource for the given basename, - * to be used as MessageSource for the corresponding theme. - *

Default implementation creates a ResourceBundleMessageSource. - * for the given basename. A subclass could create a specifically - * configured ReloadableResourceBundleMessageSource, for example. - * @param basename the basename to create a MessageSource for - * @return the MessageSource - * @see org.springframework.context.support.ResourceBundleMessageSource - * @see org.springframework.context.support.ReloadableResourceBundleMessageSource - */ - protected MessageSource createMessageSource(String basename) { - ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); - messageSource.setBasename(basename); - if (this.defaultEncoding != null) { - messageSource.setDefaultEncoding(this.defaultEncoding); - } - if (this.fallbackToSystemLocale != null) { - messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale); - } - if (this.beanClassLoader != null) { - messageSource.setBeanClassLoader(this.beanClassLoader); - } - return messageSource; - } - - /** - * Initialize the MessageSource of the given theme with the - * one from the corresponding parent of this ThemeSource. - * @param theme the Theme to (re-)initialize - */ - protected void initParent(Theme theme) { - if (theme.getMessageSource() instanceof HierarchicalMessageSource messageSource) { - if (getParentThemeSource() != null && messageSource.getParentMessageSource() == null) { - Theme parentTheme = getParentThemeSource().getTheme(theme.getName()); - if (parentTheme != null) { - messageSource.setParentMessageSource(parentTheme.getMessageSource()); - } - } - } - } - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java b/spring-context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java deleted file mode 100644 index fed03d160c8a..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/support/SimpleTheme.java +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.ui.context.support; - -import org.springframework.context.MessageSource; -import org.springframework.ui.context.Theme; -import org.springframework.util.Assert; - -/** - * Default {@link Theme} implementation, wrapping a name and an - * underlying {@link org.springframework.context.MessageSource}. - * - * @author Juergen Hoeller - * @since 17.06.2003 - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public class SimpleTheme implements Theme { - - private final String name; - - private final MessageSource messageSource; - - - /** - * Create a SimpleTheme. - * @param name the name of the theme - * @param messageSource the MessageSource that resolves theme messages - */ - public SimpleTheme(String name, MessageSource messageSource) { - Assert.notNull(name, "Name must not be null"); - Assert.notNull(messageSource, "MessageSource must not be null"); - this.name = name; - this.messageSource = messageSource; - } - - - @Override - public final String getName() { - return this.name; - } - - @Override - public final MessageSource getMessageSource() { - return this.messageSource; - } - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java b/spring-context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java deleted file mode 100644 index 879ed4690e25..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/support/UiApplicationContextUtils.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.ui.context.support; - -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; - -import org.springframework.context.ApplicationContext; -import org.springframework.ui.context.HierarchicalThemeSource; -import org.springframework.ui.context.ThemeSource; - -/** - * Utility class for UI application context implementations. - * Provides support for a special bean named "themeSource", - * of type {@link org.springframework.ui.context.ThemeSource}. - * - * @author Jean-Pierre Pawlak - * @author Juergen Hoeller - * @since 17.06.2003 - * @deprecated as of 6.0 in favor of using CSS, without direct replacement - */ -@Deprecated(since = "6.0") -public abstract class UiApplicationContextUtils { - - /** - * Name of the ThemeSource bean in the factory. - * If none is supplied, theme resolution is delegated to the parent. - * @see org.springframework.ui.context.ThemeSource - */ - public static final String THEME_SOURCE_BEAN_NAME = "themeSource"; - - - private static final Log logger = LogFactory.getLog(UiApplicationContextUtils.class); - - - /** - * Initialize the ThemeSource for the given application context, - * autodetecting a bean with the name "themeSource". If no such - * bean is found, a default (empty) ThemeSource will be used. - * @param context current application context - * @return the initialized theme source (will never be {@code null}) - * @see #THEME_SOURCE_BEAN_NAME - */ - public static ThemeSource initThemeSource(ApplicationContext context) { - if (context.containsLocalBean(THEME_SOURCE_BEAN_NAME)) { - ThemeSource themeSource = context.getBean(THEME_SOURCE_BEAN_NAME, ThemeSource.class); - // Make ThemeSource aware of parent ThemeSource. - if (context.getParent() instanceof ThemeSource pts && themeSource instanceof HierarchicalThemeSource hts) { - if (hts.getParentThemeSource() == null) { - // Only set parent context as parent ThemeSource if no parent ThemeSource - // registered already. - hts.setParentThemeSource(pts); - } - } - if (logger.isDebugEnabled()) { - logger.debug("Using ThemeSource [" + themeSource + "]"); - } - return themeSource; - } - else { - // Use default ThemeSource to be able to accept getTheme calls, either - // delegating to parent context's default or to local ResourceBundleThemeSource. - HierarchicalThemeSource themeSource = null; - if (context.getParent() instanceof ThemeSource pts) { - themeSource = new DelegatingThemeSource(); - themeSource.setParentThemeSource(pts); - } - else { - themeSource = new ResourceBundleThemeSource(); - } - if (logger.isDebugEnabled()) { - logger.debug("Unable to locate ThemeSource with name '" + THEME_SOURCE_BEAN_NAME + - "': using default [" + themeSource + "]"); - } - return themeSource; - } - } - -} diff --git a/spring-context/src/main/java/org/springframework/ui/context/support/package-info.java b/spring-context/src/main/java/org/springframework/ui/context/support/package-info.java deleted file mode 100644 index 9a6e0876238f..000000000000 --- a/spring-context/src/main/java/org/springframework/ui/context/support/package-info.java +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Classes supporting the org.springframework.ui.context package. - * Provides support classes for specialized UI contexts, for example, for web UIs. - */ -@NonNullApi -@NonNullFields -package org.springframework.ui.context.support; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-context/src/main/java/org/springframework/ui/package-info.java b/spring-context/src/main/java/org/springframework/ui/package-info.java index 96269d532a03..18e621b220f1 100644 --- a/spring-context/src/main/java/org/springframework/ui/package-info.java +++ b/spring-context/src/main/java/org/springframework/ui/package-info.java @@ -2,9 +2,7 @@ * Generic support for UI layer concepts. *

Provides generic {@code Model} and {@code ModelMap} holders for model attributes. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.ui; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java index 64b9c9b5fb1c..fc2d2b8fcea0 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractBindingResult.java @@ -27,8 +27,9 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyEditorRegistry; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -97,13 +98,13 @@ public String getObjectName() { } @Override - public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void reject(String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage) { addError(new ObjectError(getObjectName(), resolveMessageCodes(errorCode), errorArgs, defaultMessage)); } @Override public void rejectValue(@Nullable String field, String errorCode, - @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + Object @Nullable [] errorArgs, @Nullable String defaultMessage) { if (!StringUtils.hasLength(getNestedPath()) && !StringUtils.hasLength(field)) { // We're at the top of the nested object hierarchy, @@ -155,8 +156,7 @@ public List getGlobalErrors() { } @Override - @Nullable - public ObjectError getGlobalError() { + public @Nullable ObjectError getGlobalError() { for (ObjectError objectError : this.errors) { if (!(objectError instanceof FieldError)) { return objectError; @@ -177,8 +177,7 @@ public List getFieldErrors() { } @Override - @Nullable - public FieldError getFieldError() { + public @Nullable FieldError getFieldError() { for (ObjectError objectError : this.errors) { if (objectError instanceof FieldError fieldError) { return fieldError; @@ -200,8 +199,7 @@ public List getFieldErrors(String field) { } @Override - @Nullable - public FieldError getFieldError(String field) { + public @Nullable FieldError getFieldError(String field) { String fixedField = fixedField(field); for (ObjectError objectError : this.errors) { if (objectError instanceof FieldError fieldError && isMatchingFieldError(fixedField, fieldError)) { @@ -212,8 +210,7 @@ public FieldError getFieldError(String field) { } @Override - @Nullable - public Object getFieldValue(String field) { + public @Nullable Object getFieldValue(String field) { FieldError fieldError = getFieldError(field); // Use rejected value in case of error, current field value otherwise. if (fieldError != null) { @@ -237,8 +234,7 @@ else if (getTarget() != null) { * @see #getActualFieldValue */ @Override - @Nullable - public Class getFieldType(@Nullable String field) { + public @Nullable Class getFieldType(@Nullable String field) { if (getTarget() != null) { Object value = getActualFieldValue(fixedField(field)); if (value != null) { @@ -276,8 +272,7 @@ public Map getModel() { } @Override - @Nullable - public Object getRawFieldValue(String field) { + public @Nullable Object getRawFieldValue(String field) { return (getTarget() != null ? getActualFieldValue(fixedField(field)) : null); } @@ -287,8 +282,7 @@ public Object getRawFieldValue(String field) { * editor lookup facility, if available. */ @Override - @Nullable - public PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { + public @Nullable PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { PropertyEditorRegistry editorRegistry = getPropertyEditorRegistry(); if (editorRegistry != null) { Class valueTypeToUse = valueType; @@ -306,8 +300,7 @@ public PropertyEditor findEditor(@Nullable String field, @Nullable Class valu * This implementation returns {@code null}. */ @Override - @Nullable - public PropertyEditorRegistry getPropertyEditorRegistry() { + public @Nullable PropertyEditorRegistry getPropertyEditorRegistry() { return null; } @@ -378,16 +371,14 @@ public int hashCode() { * Return the wrapped target object. */ @Override - @Nullable - public abstract Object getTarget(); + public abstract @Nullable Object getTarget(); /** * Extract the actual field value for the given field. * @param field the field to check * @return the current value of the field */ - @Nullable - protected abstract Object getActualFieldValue(String field); + protected abstract @Nullable Object getActualFieldValue(String field); /** * Format the given value for the specified field. @@ -397,8 +388,7 @@ public int hashCode() { * other than from a binding error, or an actual field value) * @return the formatted value */ - @Nullable - protected Object formatFieldValue(String field, @Nullable Object value) { + protected @Nullable Object formatFieldValue(String field, @Nullable Object value) { return value; } diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java index 460d860f85a5..584d9f5162e9 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractErrors.java @@ -24,7 +24,8 @@ import java.util.List; import java.util.NoSuchElementException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** diff --git a/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java index 61675d953e17..d2dc5d84171f 100644 --- a/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/AbstractPropertyBindingResult.java @@ -18,6 +18,8 @@ import java.beans.PropertyEditor; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.PropertyAccessorUtils; @@ -25,7 +27,6 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.ConvertingPropertyEditorAdapter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,8 +44,7 @@ @SuppressWarnings("serial") public abstract class AbstractPropertyBindingResult extends AbstractBindingResult { - @Nullable - private transient ConversionService conversionService; + private transient @Nullable ConversionService conversionService; /** @@ -70,8 +70,7 @@ public void initConversion(ConversionService conversionService) { * @see #getPropertyAccessor() */ @Override - @Nullable - public PropertyEditorRegistry getPropertyEditorRegistry() { + public @Nullable PropertyEditorRegistry getPropertyEditorRegistry() { return (getTarget() != null ? getPropertyAccessor() : null); } @@ -89,8 +88,7 @@ protected String canonicalFieldName(String field) { * @see #getPropertyAccessor() */ @Override - @Nullable - public Class getFieldType(@Nullable String field) { + public @Nullable Class getFieldType(@Nullable String field) { return (getTarget() != null ? getPropertyAccessor().getPropertyType(fixedField(field)) : super.getFieldType(field)); } @@ -100,8 +98,7 @@ public Class getFieldType(@Nullable String field) { * @see #getPropertyAccessor() */ @Override - @Nullable - protected Object getActualFieldValue(String field) { + protected @Nullable Object getActualFieldValue(String field) { return getPropertyAccessor().getPropertyValue(field); } @@ -110,8 +107,7 @@ protected Object getActualFieldValue(String field) { * @see #getCustomEditor */ @Override - @Nullable - protected Object formatFieldValue(String field, @Nullable Object value) { + protected @Nullable Object formatFieldValue(String field, @Nullable Object value) { String fixedField = fixedField(field); // Try custom editor... PropertyEditor customEditor = getCustomEditor(fixedField); @@ -140,8 +136,7 @@ protected Object formatFieldValue(String field, @Nullable Object value) { * @param fixedField the fully qualified field name * @return the custom PropertyEditor, or {@code null} */ - @Nullable - protected PropertyEditor getCustomEditor(String fixedField) { + protected @Nullable PropertyEditor getCustomEditor(String fixedField) { Class targetType = getPropertyAccessor().getPropertyType(fixedField); PropertyEditor editor = getPropertyAccessor().findCustomEditor(targetType, fixedField); if (editor == null) { @@ -155,8 +150,7 @@ protected PropertyEditor getCustomEditor(String fixedField) { * if applicable. */ @Override - @Nullable - public PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { + public @Nullable PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { Class valueTypeForLookup = valueType; if (valueTypeForLookup == null) { valueTypeForLookup = getFieldType(field); diff --git a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java index 1cc6bb8e3654..54b6143b6ca0 100644 --- a/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/BeanPropertyBindingResult.java @@ -18,10 +18,11 @@ import java.io.Serializable; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanWrapper; import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; -import org.springframework.lang.Nullable; /** * Default implementation of the {@link Errors} and {@link BindingResult} @@ -43,15 +44,13 @@ @SuppressWarnings("serial") public class BeanPropertyBindingResult extends AbstractPropertyBindingResult implements Serializable { - @Nullable - private final Object target; + private final @Nullable Object target; private final boolean autoGrowNestedPaths; private final int autoGrowCollectionLimit; - @Nullable - private transient BeanWrapper beanWrapper; + private transient @Nullable BeanWrapper beanWrapper; /** @@ -81,8 +80,7 @@ public BeanPropertyBindingResult(@Nullable Object target, String objectName, @Override - @Nullable - public final Object getTarget() { + public final @Nullable Object getTarget() { return this.target; } diff --git a/spring-context/src/main/java/org/springframework/validation/BindException.java b/spring-context/src/main/java/org/springframework/validation/BindException.java index b84c81081a87..0e2ea2630cc1 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindException.java +++ b/spring-context/src/main/java/org/springframework/validation/BindException.java @@ -20,8 +20,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyEditorRegistry; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -113,7 +114,7 @@ public void reject(String errorCode, String defaultMessage) { } @Override - public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void reject(String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage) { this.bindingResult.reject(errorCode, errorArgs, defaultMessage); } @@ -129,7 +130,7 @@ public void rejectValue(@Nullable String field, String errorCode, String default @Override public void rejectValue(@Nullable String field, String errorCode, - @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + Object @Nullable [] errorArgs, @Nullable String defaultMessage) { this.bindingResult.rejectValue(field, errorCode, errorArgs, defaultMessage); } @@ -171,8 +172,7 @@ public List getGlobalErrors() { } @Override - @Nullable - public ObjectError getGlobalError() { + public @Nullable ObjectError getGlobalError() { return this.bindingResult.getGlobalError(); } @@ -192,8 +192,7 @@ public List getFieldErrors() { } @Override - @Nullable - public FieldError getFieldError() { + public @Nullable FieldError getFieldError() { return this.bindingResult.getFieldError(); } @@ -213,26 +212,22 @@ public List getFieldErrors(String field) { } @Override - @Nullable - public FieldError getFieldError(String field) { + public @Nullable FieldError getFieldError(String field) { return this.bindingResult.getFieldError(field); } @Override - @Nullable - public Object getFieldValue(String field) { + public @Nullable Object getFieldValue(String field) { return this.bindingResult.getFieldValue(field); } @Override - @Nullable - public Class getFieldType(String field) { + public @Nullable Class getFieldType(String field) { return this.bindingResult.getFieldType(field); } @Override - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { return this.bindingResult.getTarget(); } @@ -242,21 +237,18 @@ public Map getModel() { } @Override - @Nullable - public Object getRawFieldValue(String field) { + public @Nullable Object getRawFieldValue(String field) { return this.bindingResult.getRawFieldValue(field); } @Override @SuppressWarnings("rawtypes") - @Nullable - public PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { + public @Nullable PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType) { return this.bindingResult.findEditor(field, valueType); } @Override - @Nullable - public PropertyEditorRegistry getPropertyEditorRegistry() { + public @Nullable PropertyEditorRegistry getPropertyEditorRegistry() { return this.bindingResult.getPropertyEditorRegistry(); } diff --git a/spring-context/src/main/java/org/springframework/validation/BindingResult.java b/spring-context/src/main/java/org/springframework/validation/BindingResult.java index ed3e3a1d70ff..e0625d631667 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/BindingResult.java @@ -19,8 +19,9 @@ import java.beans.PropertyEditor; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyEditorRegistry; -import org.springframework.lang.Nullable; /** * General interface that represents binding results. Extends the @@ -55,8 +56,7 @@ public interface BindingResult extends Errors { * Return the wrapped target object, which may be a bean, an object with * public fields, a Map - depending on the concrete binding strategy. */ - @Nullable - Object getTarget(); + @Nullable Object getTarget(); /** * Return a model Map for the obtained state, exposing a BindingResult @@ -84,8 +84,7 @@ public interface BindingResult extends Errors { * @param field the field to check * @return the current value of the field in its raw form, or {@code null} if not known */ - @Nullable - Object getRawFieldValue(String field); + @Nullable Object getRawFieldValue(String field); /** * Find a custom property editor for the given type and property. @@ -95,16 +94,14 @@ public interface BindingResult extends Errors { * is given but should be specified in any case for consistency checking) * @return the registered editor, or {@code null} if none */ - @Nullable - PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType); + @Nullable PropertyEditor findEditor(@Nullable String field, @Nullable Class valueType); /** * Return the underlying PropertyEditorRegistry. * @return the PropertyEditorRegistry, or {@code null} if none * available for this BindingResult */ - @Nullable - PropertyEditorRegistry getPropertyEditorRegistry(); + @Nullable PropertyEditorRegistry getPropertyEditorRegistry(); /** * Resolve the given error code into message codes. diff --git a/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java b/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java index 04694d823f83..b1d3c12be4fd 100644 --- a/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/BindingResultUtils.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ public abstract class BindingResultUtils { * @return the BindingResult, or {@code null} if none found * @throws IllegalStateException if the attribute found is not of type BindingResult */ - @Nullable - public static BindingResult getBindingResult(Map model, String name) { + public static @Nullable BindingResult getBindingResult(Map model, String name) { Assert.notNull(model, "Model map must not be null"); Assert.notNull(name, "Name must not be null"); Object attr = model.get(BindingResult.MODEL_KEY_PREFIX + name); 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 3433af095bd1..46ae985424e1 100644 --- a/spring-context/src/main/java/org/springframework/validation/DataBinder.java +++ b/spring-context/src/main/java/org/springframework/validation/DataBinder.java @@ -27,7 +27,6 @@ import java.util.HashMap; import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Optional; import java.util.Set; @@ -37,6 +36,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; @@ -60,7 +60,6 @@ import org.springframework.core.convert.TypeDescriptor; import org.springframework.format.Formatter; import org.springframework.format.support.FormatterPropertyEditorAdapter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.PatternMatchUtils; @@ -146,21 +145,17 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private static final int NO_INDEX = -1; - @Nullable - private Object target; + private @Nullable Object target; - @Nullable - ResolvableType targetType; + @Nullable ResolvableType targetType; private final String objectName; - @Nullable - private AbstractPropertyBindingResult bindingResult; + private @Nullable AbstractPropertyBindingResult bindingResult; private boolean directFieldAccess = false; - @Nullable - private ExtendedTypeConverter typeConverter; + private @Nullable ExtendedTypeConverter typeConverter; private boolean declarativeBinding = false; @@ -172,30 +167,23 @@ public class DataBinder implements PropertyEditorRegistry, TypeConverter { private int autoGrowCollectionLimit = DEFAULT_AUTO_GROW_COLLECTION_LIMIT; - @Nullable - private String[] allowedFields; + private String @Nullable [] allowedFields; - @Nullable - private String[] disallowedFields; + private String @Nullable [] disallowedFields; - @Nullable - private String[] requiredFields; + private String @Nullable [] requiredFields; - @Nullable - private NameResolver nameResolver; + private @Nullable NameResolver nameResolver; - @Nullable - private ConversionService conversionService; + private @Nullable ConversionService conversionService; - @Nullable - private MessageCodesResolver messageCodesResolver; + private @Nullable MessageCodesResolver messageCodesResolver; private BindingErrorProcessor bindingErrorProcessor = new DefaultBindingErrorProcessor(); private final List validators = new ArrayList<>(); - @Nullable - private Predicate excludedValidators; + private @Nullable Predicate excludedValidators; /** @@ -225,8 +213,7 @@ public DataBinder(@Nullable Object target, String objectName) { *

If the target object is {@code null} and {@link #getTargetType()} is set, * then {@link #construct(ValueResolver)} may be called to create the target. */ - @Nullable - public Object getTarget() { + public @Nullable Object getTarget() { return this.target; } @@ -253,8 +240,7 @@ public void setTargetType(ResolvableType targetType) { * Return the {@link #setTargetType configured} type for the target object. * @since 6.1 */ - @Nullable - public ResolvableType getTargetType() { + public @Nullable ResolvableType getTargetType() { return this.targetType; } @@ -530,7 +516,7 @@ public boolean isIgnoreInvalidFields() { * @see #setDisallowedFields * @see #isAllowed(String) */ - public void setAllowedFields(@Nullable String... allowedFields) { + public void setAllowedFields(String @Nullable ... allowedFields) { this.allowedFields = PropertyAccessorUtils.canonicalPropertyNames(allowedFields); } @@ -539,8 +525,7 @@ public void setAllowedFields(@Nullable String... allowedFields) { * @return array of allowed field patterns * @see #setAllowedFields(String...) */ - @Nullable - public String[] getAllowedFields() { + public String @Nullable [] getAllowedFields() { return this.allowedFields; } @@ -550,14 +535,13 @@ public String[] getAllowedFields() { *

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

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

The default implementation of this method stores disallowed field patterns - * in {@linkplain PropertyAccessorUtils#canonicalPropertyName(String) canonical} - * form 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. @@ -568,15 +552,14 @@ public String[] getAllowedFields() { * @see #setAllowedFields * @see #isAllowed(String) */ - public void setDisallowedFields(@Nullable String... disallowedFields) { + public void setDisallowedFields(String @Nullable ... disallowedFields) { if (disallowedFields == null) { this.disallowedFields = null; } 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; } @@ -587,8 +570,7 @@ public void setDisallowedFields(@Nullable String... disallowedFields) { * @return array of disallowed field patterns * @see #setDisallowedFields(String...) */ - @Nullable - public String[] getDisallowedFields() { + public String @Nullable [] getDisallowedFields() { return this.disallowedFields; } @@ -605,7 +587,7 @@ public String[] getDisallowedFields() { * @see #setBindingErrorProcessor * @see DefaultBindingErrorProcessor#MISSING_FIELD_ERROR_CODE */ - public void setRequiredFields(@Nullable String... requiredFields) { + public void setRequiredFields(String @Nullable ... requiredFields) { this.requiredFields = PropertyAccessorUtils.canonicalPropertyNames(requiredFields); if (logger.isDebugEnabled()) { logger.debug("DataBinder requires binding of required fields [" + @@ -617,8 +599,7 @@ public void setRequiredFields(@Nullable String... requiredFields) { * Return the fields that are required for each binding process. * @return array of field names */ - @Nullable - public String[] getRequiredFields() { + public String @Nullable [] getRequiredFields() { return this.requiredFields; } @@ -639,8 +620,7 @@ public void setNameResolver(NameResolver nameResolver) { * constructor parameters. * @since 6.1 */ - @Nullable - public NameResolver getNameResolver() { + public @Nullable NameResolver getNameResolver() { return this.nameResolver; } @@ -690,7 +670,6 @@ public void setValidator(@Nullable Validator validator) { } } - @SuppressWarnings("NullAway") private void assertValidators(@Nullable Validator... validators) { Object target = getTarget(); for (Validator validator : validators) { @@ -732,8 +711,7 @@ public void replaceValidators(Validator... validators) { /** * Return the primary Validator to apply after each binding step, if any. */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return (!this.validators.isEmpty() ? this.validators.get(0) : null); } @@ -750,7 +728,6 @@ public List getValidators() { * {@link #setExcludedValidators(Predicate) exclude predicate}. * @since 6.1 */ - @SuppressWarnings("NullAway") public List getValidatorsToApply() { return (this.excludedValidators != null ? this.validators.stream().filter(validator -> !this.excludedValidators.test(validator)).toList() : @@ -777,8 +754,7 @@ public void setConversionService(@Nullable ConversionService conversionService) /** * Return the associated ConversionService, if any. */ - @Nullable - public ConversionService getConversionService() { + public @Nullable ConversionService getConversionService() { return this.conversionService; } @@ -851,36 +827,31 @@ public void registerCustomEditor(@Nullable Class requiredType, @Nullable Stri } @Override - @Nullable - public PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath) { + public @Nullable PropertyEditor findCustomEditor(@Nullable Class requiredType, @Nullable String propertyPath) { return getPropertyEditorRegistry().findCustomEditor(requiredType, propertyPath); } @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType); } @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable MethodParameter methodParam) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, methodParam); } @Override - @Nullable - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable Field field) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, field); } - @Nullable @Override - public T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, + public @Nullable T convertIfNecessary(@Nullable Object value, @Nullable Class requiredType, @Nullable TypeDescriptor typeDescriptor) throws TypeMismatchException { return getTypeConverter().convertIfNecessary(value, requiredType, typeDescriptor); @@ -920,8 +891,7 @@ public void construct(ValueResolver valueResolver) { } } - @Nullable - private Object createObject(ResolvableType objectType, String nestedPath, ValueResolver valueResolver) { + private @Nullable Object createObject(ResolvableType objectType, String nestedPath, ValueResolver valueResolver) { Class clazz = objectType.resolve(); boolean isOptional = (clazz == Optional.class); clazz = (isOptional ? objectType.resolveGeneric(0) : clazz); @@ -939,9 +909,9 @@ private Object createObject(ResolvableType objectType, String nestedPath, ValueR } else { // A single data class constructor -> resolve constructor arguments from request parameters. - String[] paramNames = BeanUtils.getParameterNames(ctor); + @Nullable String[] paramNames = BeanUtils.getParameterNames(ctor); Class[] paramTypes = ctor.getParameterTypes(); - Object[] args = new Object[paramTypes.length]; + @Nullable Object[] args = new Object[paramTypes.length]; Set failedParamNames = new HashSet<>(4); for (int i = 0; i < paramNames.length; i++) { @@ -1050,8 +1020,7 @@ private boolean hasValuesFor(String paramPath, ValueResolver resolver) { return false; } - @Nullable - private List createList( + private @Nullable List createList( String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { ResolvableType elementType = type.getNested(2); @@ -1076,8 +1045,7 @@ private List createList( return list; } - @Nullable - private Map createMap( + private @Nullable Map createMap( String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { ResolvableType elementType = type.getNested(2); @@ -1104,8 +1072,7 @@ private Map createMap( } @SuppressWarnings("unchecked") - @Nullable - private V[] createArray( + private @Nullable V @Nullable [] createArray( String paramPath, Class paramType, ResolvableType type, ValueResolver valueResolver) { ResolvableType elementType = type.getNested(2); @@ -1116,7 +1083,7 @@ private V[] createArray( int lastIndex = Math.max(indexes.last(), 0); int size = (lastIndex < this.autoGrowCollectionLimit ? lastIndex + 1: 0); - V[] array = (V[]) Array.newInstance(elementType.resolve(), size); + @Nullable V[] array = (V[]) Array.newInstance(elementType.resolve(), size); for (int index : indexes) { String indexedPath = paramPath + "[" + (index != NO_INDEX ? index : "") + "]"; @@ -1127,8 +1094,7 @@ private V[] createArray( return array; } - @Nullable - private static SortedSet getIndexes(String paramPath, ValueResolver valueResolver) { + private static @Nullable SortedSet getIndexes(String paramPath, ValueResolver valueResolver) { SortedSet indexes = null; for (String name : valueResolver.getNames()) { if (name.startsWith(paramPath + "[")) { @@ -1152,8 +1118,7 @@ private static SortedSet getIndexes(String paramPath, ValueResolver val } @SuppressWarnings("unchecked") - @Nullable - private V createIndexedValue( + private @Nullable V createIndexedValue( String paramPath, Class containerType, ResolvableType elementType, String indexedPath, ValueResolver valueResolver) { @@ -1196,7 +1161,7 @@ private void handleTypeMismatchException( } private void validateConstructorArgument( - Class constructorClass, String nestedPath, String name, @Nullable Object value) { + Class constructorClass, String nestedPath, @Nullable String name, @Nullable Object value) { Object[] hints = null; if (this.targetType != null && this.targetType.getSource() instanceof MethodParameter parameter) { @@ -1302,9 +1267,9 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) { * Determine if the given field is allowed for binding. *

Invoked for each passed-in property value. *

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

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

A field matching a disallowed pattern will not be accepted even if it @@ -1320,8 +1285,13 @@ protected void checkAllowedFields(MutablePropertyValues mpvs) { protected boolean isAllowed(String field) { String[] allowed = getAllowedFields(); String[] disallowed = getDisallowedFields(); - return ((ObjectUtils.isEmpty(allowed) || PatternMatchUtils.simpleMatch(allowed, field)) && - (ObjectUtils.isEmpty(disallowed) || !PatternMatchUtils.simpleMatch(disallowed, field.toLowerCase(Locale.ROOT)))); + if (!ObjectUtils.isEmpty(allowed) && !PatternMatchUtils.simpleMatch(allowed, field)) { + return false; + } + if (!ObjectUtils.isEmpty(disallowed)) { + return !PatternMatchUtils.simpleMatchIgnoreCase(disallowed, field); + } + return true; } /** @@ -1332,7 +1302,7 @@ protected boolean isAllowed(String field) { * @see #getBindingErrorProcessor * @see BindingErrorProcessor#processMissingFieldError */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void checkRequiredFields(MutablePropertyValues mpvs) { String[] requiredFields = getRequiredFields(); if (!ObjectUtils.isEmpty(requiredFields)) { @@ -1458,8 +1428,7 @@ public interface NameResolver { * if unresolved. For constructor parameters, the name is determined via * {@link org.springframework.core.DefaultParameterNameDiscoverer} if unresolved. */ - @Nullable - String resolveName(MethodParameter parameter); + @Nullable String resolveName(MethodParameter parameter); } @@ -1476,8 +1445,7 @@ public interface ValueResolver { * @param type the target type, based on the constructor parameter type * @return the resolved value, possibly {@code null} if none found */ - @Nullable - Object resolveValue(String name, Class type); + @Nullable Object resolveValue(String name, Class type); /** * Return the names of all property values. diff --git a/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java b/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java index a18554a5543f..c3f0b2205c59 100644 --- a/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java +++ b/spring-context/src/main/java/org/springframework/validation/DefaultMessageCodesResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -24,7 +24,8 @@ import java.util.Set; import java.util.StringJoiner; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -242,7 +243,6 @@ public String format(String errorCode, @Nullable String objectName, @Nullable St * {@link DefaultMessageCodesResolver#CODE_SEPARATOR}, skipping zero-length or * null elements altogether. */ - @SuppressWarnings("NullAway") public static String toDelimitedString(@Nullable String... elements) { StringJoiner rtn = new StringJoiner(CODE_SEPARATOR); for (String element : elements) { diff --git a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java index f01232ca1f6f..00c6c0c1ed5d 100644 --- a/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/DirectFieldBindingResult.java @@ -16,9 +16,10 @@ package org.springframework.validation; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.ConfigurablePropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; -import org.springframework.lang.Nullable; /** * Special implementation of the Errors and BindingResult interfaces, @@ -36,13 +37,11 @@ @SuppressWarnings("serial") public class DirectFieldBindingResult extends AbstractPropertyBindingResult { - @Nullable - private final Object target; + private final @Nullable Object target; private final boolean autoGrowNestedPaths; - @Nullable - private transient ConfigurablePropertyAccessor directFieldAccessor; + private transient @Nullable ConfigurablePropertyAccessor directFieldAccessor; /** @@ -68,8 +67,7 @@ public DirectFieldBindingResult(@Nullable Object target, String objectName, bool @Override - @Nullable - public final Object getTarget() { + public final @Nullable Object getTarget() { return this.target; } diff --git a/spring-context/src/main/java/org/springframework/validation/Errors.java b/spring-context/src/main/java/org/springframework/validation/Errors.java index a7292a5af9dc..cb8ff224cf4b 100644 --- a/spring-context/src/main/java/org/springframework/validation/Errors.java +++ b/spring-context/src/main/java/org/springframework/validation/Errors.java @@ -21,8 +21,9 @@ import java.util.function.Function; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.PropertyAccessor; -import org.springframework.lang.Nullable; /** * Stores and exposes information about data-binding and validation errors @@ -144,7 +145,7 @@ default void reject(String errorCode, String defaultMessage) { * @param defaultMessage fallback default message * @see #rejectValue(String, String, Object[], String) */ - void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage); + void reject(String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage); /** * Register a field error for the specified field of the current object @@ -195,7 +196,7 @@ default void rejectValue(@Nullable String field, String errorCode, String defaul * @see #reject(String, Object[], String) */ void rejectValue(@Nullable String field, String errorCode, - @Nullable Object[] errorArgs, @Nullable String defaultMessage); + Object @Nullable [] errorArgs, @Nullable String defaultMessage); /** * Add all errors from the given {@code Errors} instance to this @@ -285,8 +286,7 @@ default int getGlobalErrorCount() { * @return the global error, or {@code null} * @see #getFieldError() */ - @Nullable - default ObjectError getGlobalError() { + default @Nullable ObjectError getGlobalError() { return getGlobalErrors().stream().findFirst().orElse(null); } @@ -318,8 +318,7 @@ default int getFieldErrorCount() { * @return the field-specific error, or {@code null} * @see #getGlobalError() */ - @Nullable - default FieldError getFieldError() { + default @Nullable FieldError getFieldError() { return getFieldErrors().stream().findFirst().orElse(null); } @@ -359,8 +358,7 @@ default List getFieldErrors(String field) { * @return the field-specific error, or {@code null} * @see #getFieldError() */ - @Nullable - default FieldError getFieldError(String field) { + default @Nullable FieldError getFieldError(String field) { return getFieldErrors().stream().filter(error -> field.equals(error.getField())).findFirst().orElse(null); } @@ -373,8 +371,7 @@ default FieldError getFieldError(String field) { * @return the current value of the given field * @see #getFieldType(String) */ - @Nullable - Object getFieldValue(String field); + @Nullable Object getFieldValue(String field); /** * Determine the type of the given field, as far as possible. @@ -385,8 +382,7 @@ default FieldError getFieldError(String field) { * @return the type of the field, or {@code null} if not determinable * @see #getFieldValue(String) */ - @Nullable - default Class getFieldType(String field) { + default @Nullable Class getFieldType(String field) { return Optional.ofNullable(getFieldValue(field)).map(Object::getClass).orElse(null); } diff --git a/spring-context/src/main/java/org/springframework/validation/FieldError.java b/spring-context/src/main/java/org/springframework/validation/FieldError.java index bb57d556ff39..cc21efb743e1 100644 --- a/spring-context/src/main/java/org/springframework/validation/FieldError.java +++ b/spring-context/src/main/java/org/springframework/validation/FieldError.java @@ -16,7 +16,8 @@ package org.springframework.validation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -37,8 +38,7 @@ public class FieldError extends ObjectError { private final String field; - @Nullable - private final Object rejectedValue; + private final @Nullable Object rejectedValue; private final boolean bindingFailure; @@ -65,7 +65,7 @@ public FieldError(String objectName, String field, String defaultMessage) { * @param defaultMessage the default message to be used to resolve this message */ public FieldError(String objectName, String field, @Nullable Object rejectedValue, boolean bindingFailure, - @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + String @Nullable [] codes, Object @Nullable [] arguments, @Nullable String defaultMessage) { super(objectName, codes, arguments, defaultMessage); Assert.notNull(field, "Field must not be null"); @@ -85,8 +85,7 @@ public String getField() { /** * Return the rejected field value. */ - @Nullable - public Object getRejectedValue() { + public @Nullable Object getRejectedValue() { return this.rejectedValue; } diff --git a/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java b/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java index 41860a4c1d42..6739dd27f32e 100644 --- a/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java +++ b/spring-context/src/main/java/org/springframework/validation/MapBindingResult.java @@ -19,8 +19,8 @@ import java.io.Serializable; import java.util.Map; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -61,14 +61,12 @@ public MapBindingResult(Map target, String objectName) { } @Override - @NonNull public final Object getTarget() { return this.target; } @Override - @Nullable - protected Object getActualFieldValue(String field) { + protected @Nullable Object getActualFieldValue(String field) { return this.target.get(field); } diff --git a/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java b/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java index 48d918e9da15..ca45259d46ca 100644 --- a/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java +++ b/spring-context/src/main/java/org/springframework/validation/MessageCodeFormatter.java @@ -16,7 +16,7 @@ package org.springframework.validation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A strategy interface for formatting message codes. diff --git a/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java b/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java index 2a985e6439d2..8518a20b5f24 100644 --- a/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java +++ b/spring-context/src/main/java/org/springframework/validation/MessageCodesResolver.java @@ -16,7 +16,7 @@ package org.springframework.validation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface for building message codes from validation error codes. diff --git a/spring-context/src/main/java/org/springframework/validation/ObjectError.java b/spring-context/src/main/java/org/springframework/validation/ObjectError.java index b8187d34739e..05eca672ebbd 100644 --- a/spring-context/src/main/java/org/springframework/validation/ObjectError.java +++ b/spring-context/src/main/java/org/springframework/validation/ObjectError.java @@ -16,8 +16,9 @@ package org.springframework.validation; +import org.jspecify.annotations.Nullable; + import org.springframework.context.support.DefaultMessageSourceResolvable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ public class ObjectError extends DefaultMessageSourceResolvable { private final String objectName; - @Nullable - private transient Object source; + private transient @Nullable Object source; /** @@ -58,7 +58,7 @@ public ObjectError(String objectName, @Nullable String defaultMessage) { * @param defaultMessage the default message to be used to resolve this message */ public ObjectError( - String objectName, @Nullable String[] codes, @Nullable Object[] arguments, @Nullable String defaultMessage) { + String objectName, String @Nullable [] codes, Object @Nullable [] arguments, @Nullable String defaultMessage) { super(codes, arguments, defaultMessage); Assert.notNull(objectName, "Object name must not be null"); diff --git a/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java b/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java index 6854f26235ea..edb2c3d860c9 100644 --- a/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/SimpleErrors.java @@ -22,8 +22,9 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -88,13 +89,13 @@ public String getObjectName() { } @Override - public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void reject(String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage) { this.globalErrors.add(new ObjectError(getObjectName(), new String[] {errorCode}, errorArgs, defaultMessage)); } @Override public void rejectValue(@Nullable String field, String errorCode, - @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + Object @Nullable [] errorArgs, @Nullable String defaultMessage) { if (!StringUtils.hasLength(field)) { reject(errorCode, errorArgs, defaultMessage); @@ -123,8 +124,7 @@ public List getFieldErrors() { } @Override - @Nullable - public Object getFieldValue(String field) { + public @Nullable Object getFieldValue(String field) { FieldError fieldError = getFieldError(field); if (fieldError != null) { return fieldError.getRejectedValue(); @@ -147,8 +147,7 @@ public Object getFieldValue(String field) { } @Override - @Nullable - public Class getFieldType(String field) { + public @Nullable Class getFieldType(String field) { PropertyDescriptor pd = BeanUtils.getPropertyDescriptor(this.target.getClass(), field); if (pd != null) { return pd.getPropertyType(); diff --git a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java b/spring-context/src/main/java/org/springframework/validation/SmartValidator.java index c033a9266dac..0d4895e0c662 100644 --- a/spring-context/src/main/java/org/springframework/validation/SmartValidator.java +++ b/spring-context/src/main/java/org/springframework/validation/SmartValidator.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. @@ -16,7 +16,7 @@ package org.springframework.validation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Extended variant of the {@link Validator} interface, adding support for @@ -59,7 +59,7 @@ public interface SmartValidator extends Validator { * @see jakarta.validation.Validator#validateValue(Class, String, Object, Class[]) */ default void validateValue( - Class targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { + Class targetType, @Nullable String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { throw new IllegalArgumentException("Cannot validate individual value for " + targetType); } @@ -74,8 +74,7 @@ default void validateValue( * validator type does not match. * @since 6.1 */ - @Nullable - default T unwrap(@Nullable Class type) { + default @Nullable T unwrap(@Nullable Class type) { return null; } 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 042668317d93..7744ba60abc1 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. @@ -18,8 +18,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -68,7 +68,7 @@ public static void invokeValidator(Validator validator, Object target, Errors er * {@link Validator#supports(Class) support} the validation of the supplied object's type */ public static void invokeValidator( - Validator validator, Object target, Errors errors, @Nullable Object... validationHints) { + Validator validator, Object target, Errors errors, Object @Nullable ... validationHints) { Assert.notNull(validator, "Validator must not be null"); Assert.notNull(target, "Target object must not be null"); @@ -166,7 +166,7 @@ public static void rejectIfEmpty(Errors errors, String field, String errorCode, * @param defaultMessage fallback default message */ public static void rejectIfEmpty(Errors errors, String field, String errorCode, - @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + Object @Nullable [] errorArgs, @Nullable String defaultMessage) { Assert.notNull(errors, "Errors object must not be null"); Object value = errors.getFieldValue(field); @@ -225,7 +225,7 @@ public static void rejectIfEmptyOrWhitespace( * (can be {@code null}) */ public static void rejectIfEmptyOrWhitespace( - Errors errors, String field, String errorCode, @Nullable Object[] errorArgs) { + Errors errors, String field, String errorCode, Object @Nullable [] errorArgs) { rejectIfEmptyOrWhitespace(errors, field, errorCode, errorArgs, null); } @@ -246,11 +246,11 @@ public static void rejectIfEmptyOrWhitespace( * @param defaultMessage fallback default message */ public static void rejectIfEmptyOrWhitespace( - Errors errors, String field, String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + Errors errors, String field, String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage) { 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/annotation/ValidationAnnotationUtils.java b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java index 9a612eeda4a9..e29718758757 100644 --- a/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java +++ b/spring-context/src/main/java/org/springframework/validation/annotation/ValidationAnnotationUtils.java @@ -18,8 +18,9 @@ import java.lang.annotation.Annotation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; /** * Utility class for handling validation annotations. @@ -45,8 +46,7 @@ public abstract class ValidationAnnotationUtils { * @return the validation hints to apply (possibly an empty array), * or {@code null} if this annotation does not trigger any validation */ - @Nullable - public static Object[] determineValidationHints(Annotation ann) { + public static Object @Nullable [] determineValidationHints(Annotation ann) { // Direct presence of @Validated ? if (ann instanceof Validated validated) { return validated.value(); diff --git a/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java b/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java index 177ad9c8d98b..afce17fc8ad2 100644 --- a/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java +++ b/spring-context/src/main/java/org/springframework/validation/annotation/package-info.java @@ -5,9 +5,7 @@ *

Provides an extended variant of JSR-303's {@code @Valid}, * supporting the specification of validation groups. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.validation.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java index 57cddf3f5620..b14e3f05e047 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessor.java @@ -36,6 +36,7 @@ import jakarta.validation.metadata.PropertyDescriptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.MemberCategory; @@ -46,7 +47,6 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.KotlinDetector; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -68,8 +68,7 @@ class BeanValidationBeanRegistrationAotProcessor implements BeanRegistrationAotP @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { if (beanValidationPresent) { return BeanValidationDelegate.processAheadOfTime(registeredBean); } @@ -82,11 +81,9 @@ public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registe */ private static class BeanValidationDelegate { - @Nullable - private static final Validator validator = getValidatorIfAvailable(); + private static final @Nullable Validator validator = getValidatorIfAvailable(); - @Nullable - private static Validator getValidatorIfAvailable() { + private static @Nullable Validator getValidatorIfAvailable() { try (ValidatorFactory validator = Validation.buildDefaultValidatorFactory()) { return validator.getValidator(); } @@ -96,8 +93,7 @@ private static Validator getValidatorIfAvailable() { } } - @Nullable - public static BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public static @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { if (validator == null) { return null; } @@ -237,7 +233,7 @@ public AotContribution(Collection> validatedClasses, public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { ReflectionHints hints = generationContext.getRuntimeHints().reflection(); for (Class validatedClass : this.validatedClasses) { - hints.registerType(validatedClass, MemberCategory.DECLARED_FIELDS); + hints.registerType(validatedClass, MemberCategory.ACCESS_DECLARED_FIELDS); } for (Class> constraintValidatorClass : this.constraintValidatorClasses) { hints.registerType(constraintValidatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java index 77c7966a0ed1..d912eef2b5fe 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/BeanValidationPostProcessor.java @@ -23,13 +23,13 @@ import jakarta.validation.Validation; import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.BeanPostProcessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,8 +42,7 @@ */ public class BeanValidationPostProcessor implements BeanPostProcessor, InitializingBean { - @Nullable - private Validator validator; + private @Nullable Validator validator; private boolean afterInitialization = false; diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java index 37800c7df5f4..50f14273cd5d 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/CustomValidatorBean.java @@ -22,9 +22,9 @@ import jakarta.validation.Validator; import jakarta.validation.ValidatorContext; import jakarta.validation.ValidatorFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; /** * Configurable bean class that exposes a specific JSR-303 Validator @@ -36,14 +36,11 @@ */ public class CustomValidatorBean extends SpringValidatorAdapter implements Validator, InitializingBean { - @Nullable - private ValidatorFactory validatorFactory; + private @Nullable ValidatorFactory validatorFactory; - @Nullable - private MessageInterpolator messageInterpolator; + private @Nullable MessageInterpolator messageInterpolator; - @Nullable - private TraversableResolver traversableResolver; + private @Nullable TraversableResolver traversableResolver; /** 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 6a0f939c4f9c..b07d8ef7831d 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 @@ -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. @@ -43,6 +43,7 @@ import jakarta.validation.bootstrap.GenericBootstrap; import jakarta.validation.bootstrap.ProviderSpecificBootstrap; import org.hibernate.validator.messageinterpolation.ResourceBundleMessageInterpolator; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; @@ -51,7 +52,6 @@ import org.springframework.context.MessageSource; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.ReflectionUtils; @@ -84,37 +84,27 @@ public class LocalValidatorFactoryBean extends SpringValidatorAdapter implements ValidatorFactory, ApplicationContextAware, InitializingBean, DisposableBean { @SuppressWarnings("rawtypes") - @Nullable - private Class providerClass; + private @Nullable Class providerClass; - @Nullable - private ValidationProviderResolver validationProviderResolver; + private @Nullable ValidationProviderResolver validationProviderResolver; - @Nullable - private MessageInterpolator messageInterpolator; + private @Nullable MessageInterpolator messageInterpolator; - @Nullable - private TraversableResolver traversableResolver; + private @Nullable TraversableResolver traversableResolver; - @Nullable - private ConstraintValidatorFactory constraintValidatorFactory; + private @Nullable ConstraintValidatorFactory constraintValidatorFactory; - @Nullable - private ParameterNameDiscoverer parameterNameDiscoverer; + private @Nullable ParameterNameDiscoverer parameterNameDiscoverer; - @Nullable - private Resource[] mappingLocations; + private Resource @Nullable [] mappingLocations; private final Map validationPropertyMap = new HashMap<>(); - @Nullable - private Consumer> configurationInitializer; + private @Nullable Consumer> configurationInitializer; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private ValidatorFactory validatorFactory; + private @Nullable ValidatorFactory validatorFactory; /** @@ -342,13 +332,13 @@ private void configureParameterNameProvider(ParameterNameDiscoverer discoverer, configuration.parameterNameProvider(new ParameterNameProvider() { @Override public List getParameterNames(Constructor constructor) { - String[] paramNames = discoverer.getParameterNames(constructor); + @Nullable String[] paramNames = discoverer.getParameterNames(constructor); return (paramNames != null ? Arrays.asList(paramNames) : defaultProvider.getParameterNames(constructor)); } @Override public List getParameterNames(Method method) { - String[] paramNames = discoverer.getParameterNames(method); + @Nullable String[] paramNames = discoverer.getParameterNames(method); return (paramNames != null ? Arrays.asList(paramNames) : defaultProvider.getParameterNames(method)); } diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java index 4304848c53d6..28c971ef79f6 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationAdapter.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 jakarta.validation.ValidatorFactory; import jakarta.validation.executable.ExecutableValidator; import jakarta.validation.metadata.ConstraintDescriptor; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.support.AopUtils; @@ -50,7 +51,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.function.SingletonSupplier; import org.springframework.validation.BeanPropertyBindingResult; @@ -235,8 +235,8 @@ public Class[] determineValidationGroups(Object target, Method method) { @Override public final MethodValidationResult validateArguments( - Object target, Method method, @Nullable MethodParameter[] parameters, - Object[] arguments, Class[] groups) { + Object target, Method method, MethodParameter @Nullable [] parameters, + @Nullable Object[] arguments, Class[] groups) { Set> violations = invokeValidatorForArguments(target, method, arguments, groups); @@ -254,7 +254,7 @@ public final MethodValidationResult validateArguments( * Invoke the validator, and return the resulting violations. */ public final Set> invokeValidatorForArguments( - Object target, Method method, Object[] arguments, Class[] groups) { + Object target, Method method, @Nullable Object[] arguments, Class[] groups) { ExecutableValidator execVal = this.validator.get().forExecutables(); try { @@ -298,7 +298,7 @@ public final Set> invokeValidatorForReturnValue( private MethodValidationResult adaptViolations( Object target, Method method, Set> violations, Function parameterFunction, - Function argumentFunction) { + Function argumentFunction) { Map paramViolations = new LinkedHashMap<>(); Map nestedViolations = new LinkedHashMap<>(); @@ -461,17 +461,13 @@ private final class ParamValidationResultBuilder { private final MethodParameter parameter; - @Nullable - private final Object value; + private final @Nullable Object value; - @Nullable - private final Object container; + private final @Nullable Object container; - @Nullable - private final Integer containerIndex; + private final @Nullable Integer containerIndex; - @Nullable - private final Object containerKey; + private final @Nullable Object containerKey; private final List resolvableErrors = new ArrayList<>(); @@ -511,17 +507,13 @@ private final class ParamErrorsBuilder { private final MethodParameter parameter; - @Nullable - private final Object bean; + private final @Nullable Object bean; - @Nullable - private final Object container; + private final @Nullable Object container; - @Nullable - private final Integer containerIndex; + private final @Nullable Integer containerIndex; - @Nullable - private final Object containerKey; + private final @Nullable Object containerKey; private final Errors errors; diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java index ce39c9aa08c5..a7c9038b1291 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/MethodValidationInterceptor.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,6 +30,7 @@ import jakarta.validation.ValidatorFactory; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -40,7 +41,6 @@ import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.validation.BeanPropertyBindingResult; @@ -141,8 +141,7 @@ private MethodValidationInterceptor(MethodValidationAdapter validationAdapter, b @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { // Avoid Validator invocation on FactoryBean.getObjectType/isSingleton if (isFactoryBeanMetadataMethod(invocation.getMethod())) { return invocation.proceed(); @@ -150,7 +149,7 @@ public Object invoke(MethodInvocation invocation) throws Throwable { Object target = getTarget(invocation); Method method = invocation.getMethod(); - Object[] arguments = invocation.getArguments(); + @Nullable Object[] arguments = invocation.getArguments(); Class[] groups = determineValidationGroups(invocation); if (reactorPresent) { @@ -240,9 +239,9 @@ private static final class ReactorValidationHelper { ReactiveAdapterRegistry.getSharedInstance(); - static Object[] insertAsyncValidation( + static @Nullable Object[] insertAsyncValidation( Supplier validatorAdapterSupplier, boolean adaptViolations, - Object target, Method method, Object[] arguments) { + Object target, Method method, @Nullable Object[] arguments) { for (int i = 0; i < method.getParameterCount(); i++) { if (arguments[i] == null) { @@ -268,8 +267,7 @@ static Object[] insertAsyncValidation( return arguments; } - @Nullable - private static Class[] determineValidationGroups(Parameter parameter) { + private static Class @Nullable [] determineValidationGroups(Parameter parameter) { Validated validated = AnnotationUtils.findAnnotation(parameter, Validated.class); if (validated != null) { return validated.value(); diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java index 8e518bbc14fe..aeb98bebab49 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/SpringValidatorAdapter.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,12 +31,12 @@ import jakarta.validation.executable.ExecutableValidator; import jakarta.validation.metadata.BeanDescriptor; import jakarta.validation.metadata.ConstraintDescriptor; +import org.jspecify.annotations.Nullable; import org.springframework.beans.InvalidPropertyException; import org.springframework.beans.NotReadablePropertyException; import org.springframework.context.MessageSourceResolvable; import org.springframework.context.support.DefaultMessageSourceResolvable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -69,8 +69,7 @@ public class SpringValidatorAdapter implements SmartValidator, jakarta.validatio private static final Set internalAnnotationAttributes = Set.of("message", "groups", "payload"); - @Nullable - private jakarta.validation.Validator targetValidator; + private jakarta.validation.@Nullable Validator targetValidator; /** @@ -117,7 +116,7 @@ public void validate(Object target, Errors errors, Object... validationHints) { @SuppressWarnings({"rawtypes", "unchecked"}) @Override public void validateValue( - Class targetType, String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { + Class targetType, @Nullable String fieldName, @Nullable Object value, Errors errors, Object... validationHints) { if (this.targetValidator != null) { processConstraintViolations(this.targetValidator.validateValue( @@ -305,8 +304,7 @@ protected MessageSourceResolvable getResolvableField(String objectName, String f * @see jakarta.validation.ConstraintViolation#getInvalidValue() * @see org.springframework.validation.FieldError#getRejectedValue() */ - @Nullable - protected Object getRejectedValue(String field, ConstraintViolation violation, BindingResult bindingResult) { + protected @Nullable Object getRejectedValue(String field, ConstraintViolation violation, BindingResult bindingResult) { Object invalidValue = violation.getInvalidValue(); if (!field.isEmpty() && !field.contains("[]") && (invalidValue == violation.getLeafBean() || field.contains("[") || field.contains("."))) { @@ -423,8 +421,7 @@ public String[] getCodes() { } @Override - @Nullable - public Object[] getArguments() { + public Object @Nullable [] getArguments() { return null; } @@ -446,11 +443,9 @@ public String toString() { @SuppressWarnings("serial") private static class ViolationObjectError extends ObjectError implements Serializable { - @Nullable - private transient SpringValidatorAdapter adapter; + private @Nullable transient SpringValidatorAdapter adapter; - @Nullable - private transient ConstraintViolation violation; + private @Nullable transient ConstraintViolation violation; public ViolationObjectError(String objectName, String[] codes, Object[] arguments, ConstraintViolation violation, SpringValidatorAdapter adapter) { @@ -476,11 +471,9 @@ public boolean shouldRenderDefaultMessage() { @SuppressWarnings("serial") private static class ViolationFieldError extends FieldError implements Serializable { - @Nullable - private transient SpringValidatorAdapter adapter; + private @Nullable transient SpringValidatorAdapter adapter; - @Nullable - private transient ConstraintViolation violation; + private @Nullable transient ConstraintViolation violation; public ViolationFieldError(String objectName, String field, @Nullable Object rejectedValue, String[] codes, Object[] arguments, ConstraintViolation violation, SpringValidatorAdapter adapter) { diff --git a/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java b/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java index c367d4b45534..9f6498812eae 100644 --- a/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java +++ b/spring-context/src/main/java/org/springframework/validation/beanvalidation/package-info.java @@ -8,9 +8,7 @@ * which defines a shared ValidatorFactory/Validator setup for availability * to other Spring components. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.validation.beanvalidation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java index 69ff06f55c2b..fccb5ad41ca0 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidationResult.java @@ -81,19 +81,6 @@ default List getAllErrors() { */ List getParameterValidationResults(); - /** - * Return all validation results. This includes both method parameters with - * errors directly on them, and Object method parameters with nested errors - * on their fields and properties. - * @see #getValueResults() - * @see #getBeanResults() - * @deprecated deprecated in favor of {@link #getParameterValidationResults()} - */ - @Deprecated(since = "6.2", forRemoval = true) - default List getAllValidationResults() { - return getParameterValidationResults(); - } - /** * Return the subset of {@link #getParameterValidationResults() allValidationResults} * that includes method parameters with validation errors directly on method diff --git a/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.java b/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.java index 3a937edd4b8a..526ba41bf2ed 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.java +++ b/spring-context/src/main/java/org/springframework/validation/method/MethodValidator.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,8 +18,9 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; /** * Contract to apply method validation and handle the results. @@ -51,8 +52,8 @@ public interface MethodValidator { * @return the result of validation */ MethodValidationResult validateArguments( - Object target, Method method, @Nullable MethodParameter[] parameters, - Object[] arguments, Class[] groups); + Object target, Method method, MethodParameter @Nullable [] parameters, + @Nullable Object[] arguments, Class[] groups); /** * Delegate to {@link #validateArguments} and handle the validation result, @@ -62,8 +63,8 @@ MethodValidationResult validateArguments( * @throws MethodValidationException in case of unhandled errors. */ default void applyArgumentValidation( - Object target, Method method, @Nullable MethodParameter[] parameters, - Object[] arguments, Class[] groups) { + Object target, Method method, MethodParameter @Nullable [] parameters, + @Nullable Object[] arguments, Class[] groups) { MethodValidationResult result = validateArguments(target, method, parameters, arguments, groups); if (result.hasErrors()) { diff --git a/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java b/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java index ad3bba6b747c..81ee1235923f 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java +++ b/spring-context/src/main/java/org/springframework/validation/method/ParameterErrors.java @@ -18,8 +18,9 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.validation.Errors; import org.springframework.validation.FieldError; import org.springframework.validation.ObjectError; @@ -92,7 +93,7 @@ public void reject(String errorCode, String defaultMessage) { } @Override - public void reject(String errorCode, @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + public void reject(String errorCode, Object @Nullable [] errorArgs, @Nullable String defaultMessage) { this.errors.reject(errorCode, errorArgs, defaultMessage); } @@ -108,7 +109,7 @@ public void rejectValue(@Nullable String field, String errorCode, String default @Override public void rejectValue(@Nullable String field, String errorCode, - @Nullable Object[] errorArgs, @Nullable String defaultMessage) { + Object @Nullable [] errorArgs, @Nullable String defaultMessage) { this.errors.rejectValue(field, errorCode, errorArgs, defaultMessage); } @@ -149,8 +150,7 @@ public List getGlobalErrors() { } @Override - @Nullable - public ObjectError getGlobalError() { + public @Nullable ObjectError getGlobalError() { return this.errors.getGlobalError(); } @@ -170,8 +170,7 @@ public List getFieldErrors() { } @Override - @Nullable - public FieldError getFieldError() { + public @Nullable FieldError getFieldError() { return this.errors.getFieldError(); } @@ -191,20 +190,17 @@ public List getFieldErrors(String field) { } @Override - @Nullable - public FieldError getFieldError(String field) { + public @Nullable FieldError getFieldError(String field) { return this.errors.getFieldError(field); } @Override - @Nullable - public Object getFieldValue(String field) { + public @Nullable Object getFieldValue(String field) { return this.errors.getFieldError(field); } @Override - @Nullable - public Class getFieldType(String field) { + public @Nullable Class getFieldType(String field) { return this.errors.getFieldType(field); } diff --git a/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java b/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java index 128b078d33c6..e3a753c9b4cd 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java +++ b/spring-context/src/main/java/org/springframework/validation/method/ParameterValidationResult.java @@ -20,9 +20,10 @@ import java.util.List; import java.util.function.BiFunction; +import org.jspecify.annotations.Nullable; + import org.springframework.context.MessageSourceResolvable; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -49,19 +50,15 @@ public class ParameterValidationResult { private final MethodParameter methodParameter; - @Nullable - private final Object argument; + private final @Nullable Object argument; private final List resolvableErrors; - @Nullable - private final Object container; + private final @Nullable Object container; - @Nullable - private final Integer containerIndex; + private final @Nullable Integer containerIndex; - @Nullable - private final Object containerKey; + private final @Nullable Object containerKey; private final BiFunction, Object> sourceLookup; @@ -85,35 +82,6 @@ public ParameterValidationResult( this.sourceLookup = sourceLookup; } - /** - * Create a {@code ParameterValidationResult}. - * @deprecated in favor of - * {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object, BiFunction)} - */ - @Deprecated(since = "6.2", forRemoval = true) - public ParameterValidationResult( - MethodParameter param, @Nullable Object arg, Collection errors, - @Nullable Object container, @Nullable Integer index, @Nullable Object key) { - - this(param, arg, errors, container, index, key, (error, sourceType) -> { - throw new IllegalArgumentException("No source object of the given type"); - }); - } - - /** - * Create a {@code ParameterValidationResult}. - * @deprecated in favor of - * {@link ParameterValidationResult#ParameterValidationResult(MethodParameter, Object, Collection, Object, Integer, Object, BiFunction)} - */ - @Deprecated(since = "6.1.3", forRemoval = true) - public ParameterValidationResult( - MethodParameter param, @Nullable Object arg, Collection errors) { - - this(param, arg, errors, null, null, null, (error, sourceType) -> { - throw new IllegalArgumentException("No source object of the given type"); - }); - } - /** * The method parameter the validation results are for. @@ -125,8 +93,7 @@ public MethodParameter getMethodParameter() { /** * The method argument value that was validated. */ - @Nullable - public Object getArgument() { + public @Nullable Object getArgument() { return this.argument; } @@ -161,8 +128,7 @@ public List getResolvableErrors() { * {@link #getContainerIndex()} and {@link #getContainerKey()} provide * information about the index or key if applicable. */ - @Nullable - public Object getContainer() { + public @Nullable Object getContainer() { return this.container; } @@ -171,8 +137,7 @@ public Object getContainer() { * {@link List} or array, this method returns the index of the validated * {@link #getArgument() argument}. */ - @Nullable - public Integer getContainerIndex() { + public @Nullable Integer getContainerIndex() { return this.containerIndex; } @@ -181,8 +146,7 @@ public Integer getContainerIndex() { * key such as {@link java.util.Map}, this method returns the key of the * validated {@link #getArgument() argument}. */ - @Nullable - public Object getContainerKey() { + public @Nullable Object getContainerKey() { return this.containerKey; } diff --git a/spring-context/src/main/java/org/springframework/validation/method/package-info.java b/spring-context/src/main/java/org/springframework/validation/method/package-info.java index 001ea1b69c45..04f41ca92a6d 100644 --- a/spring-context/src/main/java/org/springframework/validation/method/package-info.java +++ b/spring-context/src/main/java/org/springframework/validation/method/package-info.java @@ -13,9 +13,7 @@ * */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.validation.method; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/validation/package-info.java b/spring-context/src/main/java/org/springframework/validation/package-info.java index cc26d2bb6776..2af4394c823a 100644 --- a/spring-context/src/main/java/org/springframework/validation/package-info.java +++ b/spring-context/src/main/java/org/springframework/validation/package-info.java @@ -2,9 +2,7 @@ * Provides data binding and validation functionality, * for usage in business and/or UI layers. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.validation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java index 3411f9a909fa..bca828e8dd85 100644 --- a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java +++ b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareConcurrentModel.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.ui.ConcurrentModel; import org.springframework.validation.BindingResult; @@ -43,8 +44,7 @@ public class BindingAwareConcurrentModel extends ConcurrentModel { @Override - @Nullable - public Object put(String key, @Nullable Object value) { + public @Nullable Object put(String key, @Nullable Object value) { removeBindingResultIfNecessary(key, value); return super.put(key, value); } diff --git a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java index c37eca8a7a0e..573c3826270b 100644 --- a/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java +++ b/spring-context/src/main/java/org/springframework/validation/support/BindingAwareModelMap.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.ui.ExtendedModelMap; import org.springframework.validation.BindingResult; diff --git a/spring-context/src/main/java/org/springframework/validation/support/package-info.java b/spring-context/src/main/java/org/springframework/validation/support/package-info.java index 355c476679f1..e7197c303bb5 100644 --- a/spring-context/src/main/java/org/springframework/validation/support/package-info.java +++ b/spring-context/src/main/java/org/springframework/validation/support/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for handling validation results. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.validation.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt index 89c607a60042..af73c5d7ba9d 100644 --- a/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.kt +++ b/spring-context/src/main/kotlin/org/springframework/context/support/BeanDefinitionDsl.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,7 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") package org.springframework.context.support import org.springframework.aot.AotDetector @@ -25,6 +26,8 @@ import org.springframework.beans.factory.getBeanProvider import org.springframework.beans.factory.support.AbstractBeanDefinition import org.springframework.beans.factory.support.BeanDefinitionReaderUtils import org.springframework.context.ApplicationContextInitializer +import org.springframework.core.ParameterizedTypeReference +import org.springframework.core.ResolvableType import org.springframework.core.env.ConfigurableEnvironment import org.springframework.core.env.Profiles import java.util.function.Supplier @@ -68,6 +71,7 @@ import java.util.function.Supplier * @see BeanDefinitionDsl * @since 5.0 */ +@Deprecated(message = "Use BeanRegistrarDsl instead") fun beans(init: BeanDefinitionDsl.() -> Unit) = BeanDefinitionDsl(init) /** @@ -79,6 +83,9 @@ fun beans(init: BeanDefinitionDsl.() -> Unit) = BeanDefinitionDsl(init) * @author Sebastien Deleuze * @since 5.0 */ +@Deprecated( + replaceWith = ReplaceWith("BeanRegistrarDsl", "org.springframework.beans.factory.BeanRegistrarDsl"), + message = "Use BeanRegistrarDsl instead") open class BeanDefinitionDsl internal constructor (private val init: BeanDefinitionDsl.() -> Unit, private val condition: (ConfigurableEnvironment) -> Boolean = { true }) : ApplicationContextInitializer { @@ -102,6 +109,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit /** * Scope enum constants. */ + @Deprecated(message = "Use BeanRegistrarDsl instead") enum class Scope { /** @@ -120,6 +128,7 @@ open class BeanDefinitionDsl internal constructor (private val init: BeanDefinit /** * Role enum constants. */ + @Deprecated(message = "Use BeanRegistrarDsl instead") enum class Role { /** 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 3ea5d627e90c..000000000000 --- 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/example/scannable/JavaxManagedBeanComponent.java b/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java deleted file mode 100644 index b3029035d874..000000000000 --- a/spring-context/src/test/java/example/scannable/JavaxManagedBeanComponent.java +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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 example.scannable; - -/** - * @author Sam Brannen - */ -@javax.annotation.ManagedBean("myJavaxManagedBeanComponent") -public class JavaxManagedBeanComponent { -} diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java index c7f56212bc4d..6dd8810ec89b 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/AspectAndAdvicePrecedenceTests.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import org.aspectj.lang.ProceedingJoinPoint; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -28,7 +29,6 @@ import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; /** * @author Adrian Colyer @@ -106,8 +106,8 @@ private static class PrecedenceVerifyingCollaborator implements PrecedenceTestAs private void checkAdvice(String whatJustHappened) { //System.out.println("[" + adviceInvocationNumber + "] " + whatJustHappened + " ==> " + EXPECTED[adviceInvocationNumber]); if (adviceInvocationNumber > (EXPECTED.length - 1)) { - throw new AssertionError("Too many advice invocations, expecting " + EXPECTED.length - + " but had " + adviceInvocationNumber); + throw new AssertionError("Too many advice invocations, expecting " + EXPECTED.length + + " but had " + adviceInvocationNumber); } String expecting = EXPECTED[adviceInvocationNumber++]; if (!whatJustHappened.equals(expecting)) { diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java index ed740ab88ce9..1c7b6740469c 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/BeanNamePointcutTests.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java index bc6f45bfc44c..34e6bc313b3d 100644 --- a/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/aspectj/autoproxy/AspectJAutoProxyCreatorTests.java @@ -29,6 +29,7 @@ import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -66,7 +67,6 @@ import org.springframework.core.DecoratingProxy; import org.springframework.core.NestedRuntimeException; import org.springframework.core.annotation.Order; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; 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 49e142044fbf..b99c0c13ccc1 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 @@ -30,6 +30,7 @@ import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -70,7 +71,6 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.core.testfixture.TimeStamped; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; diff --git a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java index d52cb7d03173..18a5e8a01b3a 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/CglibProxyTests.java @@ -20,6 +20,8 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.ClassFilter; @@ -35,8 +37,6 @@ import org.springframework.beans.testfixture.beans.TestBean; import org.springframework.context.ApplicationContextException; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java index 5ba2f290ddd0..9e7d1707a8c5 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/JdkDynamicProxyTests.java @@ -18,6 +18,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.interceptor.ExposeInvocationInterceptor; @@ -25,7 +26,6 @@ import org.springframework.beans.testfixture.beans.IOther; import org.springframework.beans.testfixture.beans.ITestBean; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java index 481fe6e591ec..273ff8978bec 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/ProxyFactoryBeanTests.java @@ -25,6 +25,7 @@ import org.aopalliance.aop.Advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -58,7 +59,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.testfixture.TimeStamped; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatException; diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java index 90731091f331..3b1615338f32 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/AutoProxyCreatorTests.java @@ -21,6 +21,7 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.TargetSource; @@ -45,7 +46,6 @@ import org.springframework.context.MessageSource; import org.springframework.context.support.StaticApplicationContext; import org.springframework.context.support.StaticMessageSource; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -414,8 +414,7 @@ public void setProxyObject(boolean proxyObject) { } @Override - @Nullable - protected Object[] getAdvicesAndAdvisorsForBean(Class beanClass, String name, @Nullable TargetSource customTargetSource) { + protected Object @Nullable [] getAdvicesAndAdvisorsForBean(Class beanClass, String name, @Nullable TargetSource customTargetSource) { if (StaticMessageSource.class.equals(beanClass)) { return DO_NOT_PROXY; } diff --git a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java index 799cbf11e05a..350e7e4138bb 100644 --- a/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java +++ b/spring-context/src/test/java/org/springframework/aop/framework/autoproxy/BeanNameAutoProxyCreatorInitTests.java @@ -18,12 +18,12 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.MethodBeforeAdvice; import org.springframework.beans.testfixture.beans.Pet; import org.springframework.context.support.ClassPathXmlApplicationContext; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java b/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java index 48d19fbf7e81..bfbcbb096779 100644 --- a/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java +++ b/spring-context/src/test/java/org/springframework/beans/factory/support/InjectAnnotationAutowireContextTests.java @@ -40,8 +40,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType; /** - * Integration tests for handling JSR-330 {@link jakarta.inject.Qualifier} and - * {@link javax.inject.Qualifier} annotations. + * Integration tests for handling {@link jakarta.inject.Qualifier} annotations. * * @author Juergen Hoeller * @author Sam Brannen @@ -317,16 +316,6 @@ void autowiredConstructorArgumentResolvesJakartaNamedCandidate() { assertThat(bean.getAnimal2().getName()).isEqualTo("Jakarta Fido"); } - @Test // gh-33345 - void autowiredConstructorArgumentResolvesJavaxNamedCandidate() { - Class testBeanClass = JavaxNamedConstructorArgumentTestBean.class; - AnnotationConfigApplicationContext context = - new AnnotationConfigApplicationContext(testBeanClass, JavaxCat.class, JavaxDog.class); - JavaxNamedConstructorArgumentTestBean bean = context.getBean(testBeanClass); - assertThat(bean.getAnimal1().getName()).isEqualTo("Javax Tiger"); - assertThat(bean.getAnimal2().getName()).isEqualTo("Javax Fido"); - } - @Test void autowiredFieldResolvesQualifiedCandidateWithDefaultValueAndNoValueOnBeanDefinition() { GenericApplicationContext context = new GenericApplicationContext(); @@ -587,29 +576,6 @@ public Animal getAnimal2() { } - static class JavaxNamedConstructorArgumentTestBean { - - private final Animal animal1; - private final Animal animal2; - - @javax.inject.Inject - public JavaxNamedConstructorArgumentTestBean(@javax.inject.Named("Cat") Animal animal1, - @javax.inject.Named("Dog") Animal animal2) { - - this.animal1 = animal1; - this.animal2 = animal2; - } - - public Animal getAnimal1() { - return this.animal1; - } - - public Animal getAnimal2() { - return this.animal2; - } - } - - public static class QualifiedFieldWithDefaultValueTestBean { @Inject @@ -705,16 +671,6 @@ public String getName() { } - @javax.inject.Named("Cat") - static class JavaxCat implements Animal { - - @Override - public String getName() { - return "Javax Tiger"; - } - } - - @jakarta.inject.Named("Dog") static class JakartaDog implements Animal { @@ -725,16 +681,6 @@ public String getName() { } - @javax.inject.Named("Dog") - static class JavaxDog implements Animal { - - @Override - public String getName() { - return "Javax Fido"; - } - } - - @Target({ElementType.FIELD, ElementType.PARAMETER, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Qualifier diff --git a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java index 9d358b81cdc2..3d524830c9bf 100644 --- a/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java +++ b/spring-context/src/test/java/org/springframework/cache/CacheReproTests.java @@ -22,6 +22,7 @@ import java.util.Optional; import java.util.concurrent.CompletableFuture; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import reactor.core.publisher.Flux; @@ -43,7 +44,6 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -531,8 +531,7 @@ public MyCacheResolver() { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { String cacheName = (String) context.getArgs()[0]; if (cacheName != null) { return Collections.singleton(cacheName); 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 c43df31de414..f8cc874eacd5 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,8 +19,11 @@ 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; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -37,10 +40,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; 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,93 +121,140 @@ void cacheHitDetermination(Class configClass) { ctx.close(); } - @Test - void cacheErrorHandlerWithLoggingCacheErrorHandler() { - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class, ErrorHandlerCachingConfiguration.class); + @ParameterizedTest + @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, + EarlyCacheHitDeterminationWithoutNullValuesConfig.class, + LateCacheHitDeterminationConfig.class, + LateCacheHitDeterminationWithValueWrapperConfig.class}) + void fluxCacheDoesntDependOnFirstRequest(Class configClass) { + + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext( + configClass, ReactiveCacheableService.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); + List l1 = service.cacheFlux(key).take(1L, true).collectList().block(); + List l2 = service.cacheFlux(key).take(3L, true).collectList().block(); + List l3 = service.cacheFlux(key).collectList().block(); - key = new Object(); + Long first = l1.get(0); - r1 = service.cacheMono(key).block(); + assertThat(l1).as("l1").containsExactly(first); + assertThat(l2).as("l2").containsExactly(first, 0L, -1L); + assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); + + 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); - key = new Object(); + r1 = service.cacheFlux(new Object()).blockFirst(); + assertThat(r1).isNotNull(); + assertThat(r1).as("cacheFlux blockFirst").isEqualTo(2L); + } - r1 = service.cacheFlux(key).blockFirst(); + @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 cacheErrorHandlerWithLoggingCacheErrorHandlerAndMethodError() { + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationException() { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveFailureCacheableService service = ctx.getBean(ReactiveFailureCacheableService.class); - Object key = new Object(); - StepVerifier.create(service.cacheMono(key)) + 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(); - key = new Object(); - StepVerifier.create(service.cacheFlux(key)) + StepVerifier.create(service.cacheFlux(new Object())) .expectErrorMessage("flux service error") .verify(); } @Test - void cacheErrorHandlerWithSimpleCacheErrorHandler() { + void cacheErrorHandlerWithLoggingCacheErrorHandlerAndOperationExceptionAndSync() { AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(ExceptionCacheManager.class, ReactiveCacheableService.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); + new AnnotationConfigApplicationContext(EarlyCacheHitDeterminationConfig.class, ReactiveSyncFailureCacheableService.class, ErrorHandlerCachingConfiguration.class); + ReactiveSyncFailureCacheableService service = ctx.getBean(ReactiveSyncFailureCacheableService.class); - Throwable completableFuturThrowable = catchThrowable(() -> service.cacheFuture(new Object()).join()); - assertThat(completableFuturThrowable).isInstanceOf(CompletionException.class) - .extracting(Throwable::getCause) - .isInstanceOf(UnsupportedOperationException.class); + assertThatExceptionOfType(CompletionException.class).isThrownBy(() -> service.cacheFuture(new Object()).join()) + .withMessage(IllegalStateException.class.getName() + ": future service error"); - Throwable monoThrowable = catchThrowable(() -> service.cacheMono(new Object()).block()); - assertThat(monoThrowable).isInstanceOf(UnsupportedOperationException.class); + StepVerifier.create(service.cacheMono(new Object())) + .expectErrorMessage("mono service error") + .verify(); - Throwable fluxThrowable = catchThrowable(() -> service.cacheFlux(new Object()).blockFirst()); - assertThat(fluxThrowable).isInstanceOf(UnsupportedOperationException.class); + StepVerifier.create(service.cacheFlux(new Object())) + .expectErrorMessage("flux service error") + .verify(); } - @ParameterizedTest - @ValueSource(classes = {EarlyCacheHitDeterminationConfig.class, - EarlyCacheHitDeterminationWithoutNullValuesConfig.class, - LateCacheHitDeterminationConfig.class, - LateCacheHitDeterminationWithValueWrapperConfig.class}) - void fluxCacheDoesntDependOnFirstRequest(Class configClass) { - - AnnotationConfigApplicationContext ctx = - new AnnotationConfigApplicationContext(configClass, ReactiveCacheableService.class); - ReactiveCacheableService service = ctx.getBean(ReactiveCacheableService.class); - - Object key = new Object(); - - List l1 = service.cacheFlux(key).take(1L, true).collectList().block(); - List l2 = service.cacheFlux(key).take(3L, true).collectList().block(); - List l3 = service.cacheFlux(key).collectList().block(); - - Long first = l1.get(0); - - assertThat(l1).as("l1").containsExactly(first); - assertThat(l2).as("l2").containsExactly(first, 0L, -1L); - assertThat(l3).as("l3").containsExactly(first, 0L, -1L, -2L, -3L); - - ctx.close(); - } @CacheConfig(cacheNames = "first") static class ReactiveCacheableService { @@ -232,16 +281,94 @@ Flux cacheFlux(Object arg) { } } + @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 @@ public void put(Object key, @Nullable Object value) { } } + @Configuration static class ErrorHandlerCachingConfiguration implements CachingConfigurer { @@ -333,6 +461,7 @@ public CacheErrorHandler errorHandler() { } } + @Configuration(proxyBeanMethods = false) @EnableCaching static class ExceptionCacheManager { @@ -345,11 +474,12 @@ protected Cache createConcurrentMapCache(String name) { 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/config/EnableCachingTests.java b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java index 923cf62ac9fd..05d8b43c939d 100644 --- a/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java +++ b/spring-context/src/test/java/org/springframework/cache/config/EnableCachingTests.java @@ -94,9 +94,10 @@ void multipleCacheManagerBeans() { assertThatThrownBy(ctx::refresh) .isInstanceOfSatisfying(NoUniqueBeanDefinitionException.class, ex -> { assertThat(ex.getMessage()).contains( - "no CacheResolver specified and expected single matching CacheManager but found 2: cm1,cm2"); + "no CacheResolver specified and expected single matching CacheManager but found 2") + .contains("cm1", "cm2"); assertThat(ex.getNumberOfBeansFound()).isEqualTo(2); - assertThat(ex.getBeanNamesFound()).containsExactly("cm1", "cm2"); + assertThat(ex.getBeanNamesFound()).containsExactlyInAnyOrder("cm1", "cm2"); }).hasNoCause(); } diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java index 3b826775a545..75ff994160ed 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheOperationExpressionEvaluatorTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.Iterator; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanFactory; @@ -36,7 +37,6 @@ import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java index 4e5cafd54b9d..39fad9892b8a 100644 --- a/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java +++ b/spring-context/src/test/java/org/springframework/cache/interceptor/CacheResolverCustomizationTests.java @@ -21,6 +21,7 @@ import java.util.Collections; import java.util.concurrent.atomic.AtomicLong; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -37,7 +38,6 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.testfixture.cache.CacheTestUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -260,8 +260,7 @@ private RuntimeCacheResolver(CacheManager cacheManager) { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { String cacheName = (String) context.getArgs()[1]; return Collections.singleton(cacheName); } @@ -275,8 +274,7 @@ private NullCacheResolver(CacheManager cacheManager) { } @Override - @Nullable - protected Collection getCacheNames(CacheOperationInvocationContext context) { + protected @Nullable Collection getCacheNames(CacheOperationInvocationContext context) { return null; } } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java index 574038a83b3d..5d2d23b9c9c3 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationBeanNameGeneratorTests.java @@ -23,10 +23,7 @@ import java.util.List; import example.scannable.DefaultNamedComponent; -import example.scannable.JakartaManagedBeanComponent; import example.scannable.JakartaNamedComponent; -import example.scannable.JavaxManagedBeanComponent; -import example.scannable.JavaxNamedComponent; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; @@ -108,21 +105,6 @@ void generateBeanNameWithJakartaNamedComponent() { assertGeneratedName(JakartaNamedComponent.class, "myJakartaNamedComponent"); } - @Test - void generateBeanNameWithJavaxNamedComponent() { - assertGeneratedName(JavaxNamedComponent.class, "myJavaxNamedComponent"); - } - - @Test - void generateBeanNameWithJakartaManagedBeanComponent() { - assertGeneratedName(JakartaManagedBeanComponent.class, "myJakartaManagedBeanComponent"); - } - - @Test - void generateBeanNameWithJavaxManagedBeanComponent() { - assertGeneratedName(JavaxManagedBeanComponent.class, "myJavaxManagedBeanComponent"); - } - @Test void generateBeanNameWithCustomStereotypeComponent() { assertGeneratedName(DefaultNamedComponent.class, "thoreau"); @@ -168,18 +150,14 @@ void generateBeanNameFromSubStereotypeAnnotationWithStringArrayValueAndExplicitC assertGeneratedName(RestControllerAdviceClass.class, "myRestControllerAdvice"); } - @Test // gh-34317 + @Test // gh-34317, gh-34346 void generateBeanNameFromStereotypeAnnotationWithStringValueAsExplicitAliasForMetaAnnotationOtherThanComponent() { - // As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name. - // As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName". - assertGeneratedName(StereotypeWithoutExplicitName.class, "enigma"); + assertGeneratedName(StereotypeWithoutExplicitName.class, "annotationBeanNameGeneratorTests.StereotypeWithoutExplicitName"); } - @Test // gh-34317 + @Test // gh-34317, gh-34346 void generateBeanNameFromStereotypeAnnotationWithStringValueAndExplicitAliasForComponentNameWithBlankName() { - // As of Spring Framework 6.2, "enigma" is incorrectly used as the @Component name. - // As of Spring Framework 7.0, the generated name will be "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName". - assertGeneratedName(StereotypeWithGeneratedName.class, "enigma"); + assertGeneratedName(StereotypeWithGeneratedName.class, "annotationBeanNameGeneratorTests.StereotypeWithGeneratedName"); } @Test // gh-34317 diff --git a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java index 80f174db288f..7b728f18a33a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/AnnotationConfigApplicationContextTests.java @@ -20,6 +20,7 @@ import java.util.Objects; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; @@ -38,7 +39,6 @@ import org.springframework.context.testfixture.context.annotation.CglibConfiguration; import org.springframework.context.testfixture.context.annotation.LambdaBeanConfiguration; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import static java.lang.String.format; @@ -534,7 +534,7 @@ void refreshForAotRegisterHintsForCglibProxy() { TypeReference cglibType = TypeReference.of(CglibConfiguration.class.getName() + "$$SpringCGLIB$$0"); assertThat(RuntimeHintsPredicates.reflection().onType(cglibType) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS)) + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.ACCESS_DECLARED_FIELDS)) .accepts(runtimeHints); assertThat(RuntimeHintsPredicates.reflection().onType(CglibConfiguration.class) .withMemberCategories(MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS)) 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 dda782ce8979..ed5e45b85b85 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,15 +16,27 @@ 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; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.core.SpringProperties; 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; @@ -35,7 +47,7 @@ class BackgroundBootstrapTests { @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThread() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadBeanConfig.class); @@ -45,7 +57,7 @@ void bootstrapWithUnmanagedThread() { } @Test - @Timeout(5) + @Timeout(10) @EnabledForTestGroups(LONG_RUNNING) void bootstrapWithUnmanagedThreads() { ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(UnmanagedThreadsBeanConfig.class); @@ -57,17 +69,95 @@ void bootstrapWithUnmanagedThreads() { } @Test - @Timeout(5) + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + void bootstrapWithStrictLockingFlag() { + SpringProperties.setFlag(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME); + try { + ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(StrictLockingBeanConfig.class); + assertThat(ctx.getBean("testBean2", TestBean.class).getSpouse()).isSameAs(ctx.getBean("testBean1")); + ctx.close(); + } + finally { + SpringProperties.setProperty(DefaultListableBeanFactory.STRICT_LOCKING_PROPERTY_NAME, null); + } + } + + @Test + @Timeout(10) + @EnabledForTestGroups(LONG_RUNNING) + 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 bootstrapWithCircularReference() { - ConfigurableApplicationContext ctx = new AnnotationConfigApplicationContext(CircularReferenceBeanConfig.class); + 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); @@ -78,6 +168,24 @@ void bootstrapWithCustomExecutor() { 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 { @@ -89,7 +197,7 @@ public TestBean testBean1(ObjectProvider testBean2) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -100,7 +208,7 @@ public TestBean testBean2() { Thread.sleep(2000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -120,7 +228,7 @@ public TestBean testBean1(ObjectProvider testBean3, ObjectProvider 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(); + } + }; + } + } + + + @Configuration(proxyBeanMethods = false) + static class StrictLockingBeanConfig { + + @Bean + public TestBean testBean1(ObjectProvider testBean2) { + new Thread(testBean2::getObject).start(); + 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 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 CircularReferenceBeanConfig { + static class CircularReferenceAgainstMainThreadBeanConfig { @Bean public TestBean testBean1(ObjectProvider testBean2) { @@ -158,7 +319,7 @@ public TestBean testBean1(ObjectProvider testBean2) { Thread.sleep(1000); } catch (InterruptedException ex) { - throw new RuntimeException(ex); + Thread.currentThread().interrupt(); } return new TestBean(); } @@ -169,13 +330,128 @@ public TestBean testBean2(TestBean testBean1) { 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 { @@ -190,13 +466,13 @@ public ThreadPoolTaskExecutor bootstrapExecutor() { @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/BeanMethodPolymorphismTests.java b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java index 1b89d020bbc0..880e79edea6f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/BeanMethodPolymorphismTests.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. @@ -242,7 +242,8 @@ static class Config extends BaseConfig { @Configuration static class OverridingConfig extends BaseConfig { - @Bean @Lazy + @Bean + @Lazy @Override public BaseTestBean testBean() { return new BaseTestBean() { @@ -258,7 +259,8 @@ public String toString() { @Configuration static class OverridingConfigWithDifferentBeanName extends BaseConfig { - @Bean("myTestBean") @Lazy + @Bean("myTestBean") + @Lazy @Override public BaseTestBean testBean() { return new BaseTestBean() { @@ -274,7 +276,8 @@ public String toString() { @Configuration static class NarrowedOverridingConfig extends BaseConfig { - @Bean @Lazy + @Bean + @Lazy @Override public ExtendedTestBean testBean() { return new ExtendedTestBean() { @@ -287,6 +290,7 @@ public String toString() { } + @SuppressWarnings("deprecation") @Configuration(enforceUniqueMethods = false) static class ConfigWithOverloading { @@ -302,15 +306,18 @@ String aString(Integer dependency) { } + @SuppressWarnings("deprecation") @Configuration(enforceUniqueMethods = false) static class ConfigWithOverloadingAndAdditionalMetadata { - @Bean @Lazy + @Bean + @Lazy String aString() { return "regular"; } - @Bean @Lazy + @Bean + @Lazy String aString(Integer dependency) { return "overloaded" + dependency; } @@ -335,7 +342,8 @@ Integer anInt() { return 5; } - @Bean @Lazy + @Bean + @Lazy String aString(Integer dependency) { return "overloaded" + dependency; } @@ -350,7 +358,8 @@ Integer anInt() { return 5; } - @Bean @Lazy + @Bean + @Lazy String aString(List dependency) { return "overloaded" + dependency.get(0); } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java index f7880f4910dc..48c6f6db728d 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ClassPathScanningCandidateComponentProviderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,10 +27,7 @@ import java.util.stream.Stream; import example.gh24375.AnnotatedComponent; -import example.indexed.IndexedJakartaManagedBeanComponent; import example.indexed.IndexedJakartaNamedComponent; -import example.indexed.IndexedJavaxManagedBeanComponent; -import example.indexed.IndexedJavaxNamedComponent; import example.profilescan.DevComponent; import example.profilescan.ProfileAnnotatedComponent; import example.profilescan.ProfileMetaAnnotatedComponent; @@ -40,10 +37,7 @@ import example.scannable.FooDao; import example.scannable.FooService; import example.scannable.FooServiceImpl; -import example.scannable.JakartaManagedBeanComponent; import example.scannable.JakartaNamedComponent; -import example.scannable.JavaxManagedBeanComponent; -import example.scannable.JavaxNamedComponent; import example.scannable.MessageBean; import example.scannable.NamedComponent; import example.scannable.NamedStubDao; @@ -99,51 +93,31 @@ class ClassPathScanningCandidateComponentProviderTests { BarComponent.class ); - private static final Set> scannedJakartaComponents = Set.of( - JakartaNamedComponent.class, - JakartaManagedBeanComponent.class - ); - - private static final Set> scannedJavaxComponents = Set.of( - JavaxNamedComponent.class, - JavaxManagedBeanComponent.class - ); - - private static final Set> indexedComponents = Set.of( - IndexedJakartaNamedComponent.class, - IndexedJakartaManagedBeanComponent.class, - IndexedJavaxNamedComponent.class, - IndexedJavaxManagedBeanComponent.class - ); - @Test void defaultsWithScan() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); provider.setResourceLoader(new DefaultResourceLoader( CandidateComponentsTestClassLoader.disableIndex(getClass().getClassLoader()))); - testDefault(provider, TEST_BASE_PACKAGE, true, true, false); + testDefault(provider, TEST_BASE_PACKAGE, true, false); } @Test void defaultsWithIndex() { ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(true); provider.setResourceLoader(new DefaultResourceLoader(TEST_BASE_CLASSLOADER)); - testDefault(provider, "example", true, true, true); + testDefault(provider, "example", true, true); } private void testDefault(ClassPathScanningCandidateComponentProvider provider, String basePackage, - boolean includeScannedJakartaComponents, boolean includeScannedJavaxComponents, boolean includeIndexedComponents) { + boolean includeScannedJakartaComponents, boolean includeIndexedComponents) { Set> expectedTypes = new HashSet<>(springComponents); if (includeScannedJakartaComponents) { - expectedTypes.addAll(scannedJakartaComponents); - } - if (includeScannedJavaxComponents) { - expectedTypes.addAll(scannedJavaxComponents); + expectedTypes.add(JakartaNamedComponent.class); } if (includeIndexedComponents) { - expectedTypes.addAll(indexedComponents); + expectedTypes.add(IndexedJakartaNamedComponent.class); } Set candidates = provider.findCandidateComponents(basePackage); @@ -216,7 +190,7 @@ void customAnnotationTypeIncludeFilterWithIndex() { private void testCustomAnnotationTypeIncludeFilter(ClassPathScanningCandidateComponentProvider provider) { provider.addIncludeFilter(new AnnotationTypeFilter(Component.class)); - testDefault(provider, TEST_BASE_PACKAGE, false, false, false); + testDefault(provider, TEST_BASE_PACKAGE, false, false); } @Test @@ -309,7 +283,7 @@ private void testExclude(ClassPathScanningCandidateComponentProvider provider) { Set candidates = provider.findCandidateComponents(TEST_BASE_PACKAGE); assertScannedBeanDefinitions(candidates); assertBeanTypes(candidates, FooServiceImpl.class, StubFooDao.class, ServiceInvocationCounter.class, - BarComponent.class, JakartaManagedBeanComponent.class, JavaxManagedBeanComponent.class); + BarComponent.class); } @Test 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 b08c67573eb2..bfd003c53328 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. @@ -116,16 +116,6 @@ void postConstructAndPreDestroyWithApplicationContextAndPostProcessor() { assertThat(bean.destroyCalled).isTrue(); } - @Test - void postConstructAndPreDestroyWithLegacyAnnotations() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LegacyAnnotatedInitDestroyBean.class)); - - LegacyAnnotatedInitDestroyBean bean = (LegacyAnnotatedInitDestroyBean) bf.getBean("annotatedBean"); - assertThat(bean.initCalled).isTrue(); - bf.destroySingletons(); - assertThat(bean.destroyCalled).isTrue(); - } - @Test void postConstructAndPreDestroyWithManualConfiguration() { InitDestroyAnnotationBeanPostProcessor bpp = new InitDestroyAnnotationBeanPostProcessor(); @@ -223,26 +213,6 @@ void resourceInjectionWithPrototypes() { assertThat(bean.destroy3Called).isTrue(); } - @Test - void resourceInjectionWithLegacyAnnotations() { - bf.registerBeanDefinition("annotatedBean", new RootBeanDefinition(LegacyResourceInjectionBean.class)); - TestBean tb = new TestBean(); - bf.registerSingleton("testBean", tb); - TestBean tb2 = new TestBean(); - bf.registerSingleton("testBean2", tb2); - - LegacyResourceInjectionBean bean = (LegacyResourceInjectionBean) bf.getBean("annotatedBean"); - assertThat(bean.initCalled).isTrue(); - assertThat(bean.init2Called).isTrue(); - assertThat(bean.init3Called).isTrue(); - assertThat(bean.getTestBean()).isSameAs(tb); - assertThat(bean.getTestBean2()).isSameAs(tb2); - bf.destroySingletons(); - assertThat(bean.destroyCalled).isTrue(); - assertThat(bean.destroy2Called).isTrue(); - assertThat(bean.destroy3Called).isTrue(); - } - @Test void resourceInjectionWithResolvableDependencyType() { bpp.setBeanFactory(bf); @@ -257,7 +227,7 @@ void resourceInjectionWithResolvableDependencyType() { 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"); @@ -342,7 +312,7 @@ void extendedResourceInjection() { 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"); @@ -393,7 +363,7 @@ void extendedResourceInjectionWithOverriding() { 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"); @@ -558,30 +528,6 @@ private void destroy() { } - public static class LegacyAnnotatedInitDestroyBean { - - public boolean initCalled = false; - - public boolean destroyCalled = false; - - @javax.annotation.PostConstruct - private void init() { - if (this.initCalled) { - throw new IllegalStateException("Already called"); - } - this.initCalled = true; - } - - @javax.annotation.PreDestroy - private void destroy() { - if (this.destroyCalled) { - throw new IllegalStateException("Already called"); - } - this.destroyCalled = true; - } - } - - public static class InitDestroyBeanPostProcessor implements DestructionAwareBeanPostProcessor { @Override @@ -691,83 +637,6 @@ public TestBean getTestBean2() { } - public static class LegacyResourceInjectionBean extends LegacyAnnotatedInitDestroyBean { - - public boolean init2Called = false; - - public boolean init3Called = false; - - public boolean destroy2Called = false; - - public boolean destroy3Called = false; - - @javax.annotation.Resource - private TestBean testBean; - - private TestBean testBean2; - - @javax.annotation.PostConstruct - protected void init2() { - if (this.testBean == null || this.testBean2 == null) { - throw new IllegalStateException("Resources not injected"); - } - if (!this.initCalled) { - throw new IllegalStateException("Superclass init method not called yet"); - } - if (this.init2Called) { - throw new IllegalStateException("Already called"); - } - this.init2Called = true; - } - - @javax.annotation.PostConstruct - private void init() { - if (this.init3Called) { - throw new IllegalStateException("Already called"); - } - this.init3Called = true; - } - - @javax.annotation.PreDestroy - protected void destroy2() { - if (this.destroyCalled) { - throw new IllegalStateException("Superclass destroy called too soon"); - } - if (this.destroy2Called) { - throw new IllegalStateException("Already called"); - } - this.destroy2Called = true; - } - - @javax.annotation.PreDestroy - private void destroy() { - if (this.destroyCalled) { - throw new IllegalStateException("Superclass destroy called too soon"); - } - if (this.destroy3Called) { - throw new IllegalStateException("Already called"); - } - this.destroy3Called = true; - } - - @javax.annotation.Resource - public void setTestBean2(TestBean testBean2) { - if (this.testBean2 != null) { - throw new IllegalStateException("Already called"); - } - this.testBean2 = testBean2; - } - - public TestBean getTestBean() { - return testBean; - } - - public TestBean getTestBean2() { - return testBean2; - } - } - - static class NonPublicResourceInjectionBean extends ResourceInjectionBean { @Resource(name="testBean4", type=TestBean.class) diff --git a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java index 7f4e591bc4f4..0356e01ae23f 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/CommonAnnotationBeanRegistrationAotContributionTests.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 @@ void contributeWhenPrivateFieldInjectionInjectsUsingReflection() { RegisteredBean registeredBean = getAndApplyContribution( PrivateFieldResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PrivateFieldResourceSample.class, "one")) + .onType(PrivateFieldResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateFieldResourceSample instance = new PrivateFieldResourceSample(); @@ -98,7 +98,7 @@ void contributeWhenPackagePrivateFieldInjectionInjectsUsingFieldAssignement() { RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateFieldResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PackagePrivateFieldResourceSample.class, "one")) + .onType(PackagePrivateFieldResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateFieldResourceSample instance = new PackagePrivateFieldResourceSample(); @@ -117,7 +117,7 @@ void contributeWhenPackagePrivateFieldInjectionOnParentClassInjectsUsingReflecti RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateFieldResourceFromParentSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onField(PackagePrivateFieldResourceSample.class, "one")) + .onType(PackagePrivateFieldResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateFieldResourceFromParentSample instance = new PackagePrivateFieldResourceFromParentSample(); @@ -135,7 +135,7 @@ void contributeWhenPrivateMethodInjectionInjectsUsingReflection() { RegisteredBean registeredBean = getAndApplyContribution( PrivateMethodResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PrivateMethodResourceSample.class, "setOne").invoke()) + .onMethodInvocation(PrivateMethodResourceSample.class, "setOne")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateMethodResourceSample instance = new PrivateMethodResourceSample(); @@ -153,7 +153,7 @@ void contributeWhenPrivateMethodInjectionWithCustomNameInjectsUsingReflection() RegisteredBean registeredBean = getAndApplyContribution( PrivateMethodResourceWithCustomNameSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PrivateMethodResourceWithCustomNameSample.class, "setText").invoke()) + .onMethodInvocation(PrivateMethodResourceWithCustomNameSample.class, "setText")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PrivateMethodResourceWithCustomNameSample instance = new PrivateMethodResourceWithCustomNameSample(); @@ -172,7 +172,7 @@ void contributeWhenPackagePrivateMethodInjectionInjectsUsingMethodInvocation() { RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateMethodResourceSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PackagePrivateMethodResourceSample.class, "setOne").introspect()) + .onType(PackagePrivateMethodResourceSample.class)) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateMethodResourceSample instance = new PackagePrivateMethodResourceSample(); @@ -191,7 +191,7 @@ void contributeWhenPackagePrivateMethodInjectionOnParentClassInjectsUsingReflect RegisteredBean registeredBean = getAndApplyContribution( PackagePrivateMethodResourceFromParentSample.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(PackagePrivateMethodResourceSample.class, "setOne")) + .onMethodInvocation(PackagePrivateMethodResourceSample.class, "setOne")) .accepts(this.generationContext.getRuntimeHints()); compile(registeredBean, (postProcessor, compiled) -> { PackagePrivateMethodResourceFromParentSample instance = new PackagePrivateMethodResourceFromParentSample(); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java index 91876dbdf6a3..b7866f31bc67 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassAndBeanMethodTests.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. @@ -128,7 +128,7 @@ void verifyToString() throws Exception { .startsWith("ConfigurationClass: beanName 'Config1', class path resource"); List beanMethods = getBeanMethods(configurationClass); - String prefix = "BeanMethod: " + Config1.class.getName(); + String prefix = "BeanMethod: java.lang.String " + Config1.class.getName(); assertThat(beanMethods.get(0).toString()).isEqualTo(prefix + ".bean0()"); assertThat(beanMethods.get(1).toString()).isEqualTo(prefix + ".bean1(java.lang.String)"); assertThat(beanMethods.get(2).toString()).isEqualTo(prefix + ".bean2(java.lang.String,java.lang.Integer)"); diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassEnhancerTests.java index 052f27f43f22..ff801346c764 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 @@ -23,11 +23,11 @@ import java.security.ProtectionDomain; import java.security.SecureClassLoader; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.OverridingClassLoader; import org.springframework.core.SmartClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.StreamUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -76,7 +76,7 @@ void withPublicClass() { classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithPublicClass.class, classLoader); assertThat(MyConfigWithPublicClass.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); } @Test @@ -104,6 +104,31 @@ void withNonPublicClass() { assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); } + @Test + void withNonPublicConstructor() { + ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); + + ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); + Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new OverridingClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new CustomSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + + classLoader = new BasicSmartClassLoader(getClass().getClassLoader()); + enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicConstructor.class, classLoader); + assertThat(MyConfigWithNonPublicConstructor.class).isAssignableFrom(enhancedClass); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); + } + @Test void withNonPublicMethod() { ConfigurationClassEnhancer configurationClassEnhancer = new ConfigurationClassEnhancer(); @@ -111,7 +136,7 @@ void withNonPublicMethod() { ClassLoader classLoader = new URLClassLoader(new URL[0], getClass().getClassLoader()); Class enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); assertThat(MyConfigWithNonPublicMethod.class).isAssignableFrom(enhancedClass); - assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader); + assertThat(enhancedClass.getClassLoader()).isEqualTo(classLoader.getParent()); classLoader = new OverridingClassLoader(getClass().getClassLoader()); enhancedClass = configurationClassEnhancer.enhance(MyConfigWithNonPublicMethod.class, classLoader); @@ -160,6 +185,19 @@ public String myBean() { } + @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/ConfigurationClassPostProcessorAotContributionTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java index 9b7777cbf9f9..ab54eb1b6208 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorAotContributionTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.context.annotation; +import java.lang.reflect.Constructor; import java.util.ArrayList; import java.util.List; import java.util.function.BiConsumer; @@ -24,7 +25,9 @@ import javax.lang.model.element.Modifier; +import jakarta.annotation.PostConstruct; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -36,7 +39,10 @@ import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.ListableBeanFactory; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; import org.springframework.beans.factory.config.BeanPostProcessor; import org.springframework.beans.factory.support.DefaultListableBeanFactory; @@ -53,6 +59,7 @@ import org.springframework.context.testfixture.context.generator.SimpleComponent; import org.springframework.core.Ordered; import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.DefaultPropertySourceFactory; import org.springframework.core.test.tools.Compiled; @@ -61,7 +68,6 @@ import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -74,8 +80,9 @@ * @author Phillip Webb * @author Stephane Nicoll * @author Sam Brannen + * @author Sebastien Deleuze */ -class ConfigurationClassPostProcessorAotContributionTests { +public class ConfigurationClassPostProcessorAotContributionTests { private final TestGenerationContext generationContext = new TestGenerationContext(); @@ -223,9 +230,8 @@ public void setImportMetadata(AnnotationMetadata importMetadata) { this.metadata = importMetadata; } - @Nullable @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (beanName.equals("testProcessing")) { return this.metadata; } @@ -440,9 +446,181 @@ private RegisteredBean getRegisteredBean(Class bean) { } } + @Nested + public class BeanRegistrarTests { + + @Test + void applyToWhenHasDefaultConstructor() throws NoSuchMethodException { + BeanFactoryInitializationAotContribution contribution = getContribution(DefaultConstructorConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + Constructor fooConstructor = Foo.class.getDeclaredConstructor(); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(RuntimeHintsPredicates.reflection().onConstructorInvocation(fooConstructor)) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + @Test + void applyToWhenHasInstanceSupplier() { + BeanFactoryInitializationAotContribution contribution = getContribution(InstanceSupplierConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(Foo.class)).isNotNull(); + assertThat(generationContext.getRuntimeHints().reflection().getTypeHint(Foo.class)).isNull(); + freshContext.close(); + }); + } + + @Test + void applyToWhenHasPostConstructAnnotationPostProcessed() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + PostConstructConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + Init init = freshContext.getBean(Init.class); + assertThat(init).isNotNull(); + assertThat(init.initialized).isTrue(); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Init.class, "postConstruct")) + .accepts(generationContext.getRuntimeHints()); + freshContext.close(); + }); + } + + @Test + void applyToWhenIsImportAware() { + BeanFactoryInitializationAotContribution contribution = getContribution(CommonAnnotationBeanPostProcessor.class, + ImportAwareBeanRegistrarConfiguration.class); + assertThat(contribution).isNotNull(); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + compile((initializer, compiled) -> { + GenericApplicationContext freshContext = new GenericApplicationContext(); + initializer.accept(freshContext); + freshContext.refresh(); + assertThat(freshContext.getBean(ClassNameHolder.class).className()) + .isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName()); + freshContext.close(); + }); + } + + @SuppressWarnings("unchecked") + private void compile(BiConsumer, Compiled> result) { + MethodReference methodReference = beanFactoryInitializationCode.getInitializers().get(0); + beanFactoryInitializationCode.getTypeBuilder().set(type -> { + ArgumentCodeGenerator argCodeGenerator = ArgumentCodeGenerator + .of(ListableBeanFactory.class, "applicationContext.getBeanFactory()") + .and(ArgumentCodeGenerator.of(Environment.class, "applicationContext.getEnvironment()")); + CodeBlock methodInvocation = methodReference.toInvokeCodeBlock(argCodeGenerator, + beanFactoryInitializationCode.getClassName()); + type.addModifiers(Modifier.PUBLIC); + type.addSuperinterface(ParameterizedTypeName.get(Consumer.class, GenericApplicationContext.class)); + type.addMethod(MethodSpec.methodBuilder("accept").addModifiers(Modifier.PUBLIC) + .addParameter(GenericApplicationContext.class, "applicationContext") + .addStatement(methodInvocation) + .build()); + }); + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().with(generationContext).compile(compiled -> + result.accept(compiled.getInstance(Consumer.class), compiled)); + } + + + @Configuration + @Import(DefaultConstructorBeanRegistrar.class) + public static class DefaultConstructorConfiguration { + } + + public static class DefaultConstructorBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class); + } + } + + @Configuration + @Import(InstanceSupplierBeanRegistrar.class) + public static class InstanceSupplierConfiguration { + } + + public static class InstanceSupplierBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Foo.class, spec -> spec.supplier(context -> new Foo())); + } + } + + @Configuration + @Import(PostConstructBeanRegistrar.class) + public static class PostConstructConfiguration { + } + + public static class PostConstructBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(Init.class); + } + } + + @Import(ImportAwareBeanRegistrar.class) + public static class ImportAwareBeanRegistrarConfiguration { + } + + public static class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware { + + @Nullable + private AnnotationMetadata importMetadata; + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context -> + new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName()))); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + public @Nullable AnnotationMetadata getImportMetadata() { + return this.importMetadata; + } + } + + static class Foo { + } + + static class Init { + + boolean initialized = false; + + @PostConstruct + void postConstruct() { + initialized = true; + } + } + + } + + public record ClassNameHolder(@Nullable String className) {} + - @Nullable - private BeanFactoryInitializationAotContribution getContribution(Class... types) { + private @Nullable BeanFactoryInitializationAotContribution getContribution(Class... types) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); for (Class type : types) { beanFactory.registerBeanDefinition(type.getName(), new RootBeanDefinition(type)); 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 122111aee981..2add6155bf5a 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ConfigurationClassPostProcessorTests.java @@ -129,6 +129,22 @@ void enhancementIsPresentBecauseSingletonSemanticsAreRespectedUsingAsm() { assertThat(beanFactory.getDependentBeans("config")).contains("bar"); } + @Test // gh-34663 + void enhancementIsPresentForAbstractConfigClassWithoutBeanMethods() { + beanFactory.registerBeanDefinition("config", new RootBeanDefinition(AbstractConfigWithoutBeanMethods.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + RootBeanDefinition beanDefinition = (RootBeanDefinition) beanFactory.getBeanDefinition("config"); + assertThat(beanDefinition.hasBeanClass()).isTrue(); + assertThat(beanDefinition.getBeanClass().getName()).contains(ClassUtils.CGLIB_CLASS_SEPARATOR); + Foo foo = beanFactory.getBean("foo", Foo.class); + Bar bar = beanFactory.getBean("bar", Bar.class); + assertThat(bar.foo).isSameAs(foo); + assertThat(beanFactory.getDependentBeans("foo")).contains("bar"); + String[] dependentsOfSingletonBeanConfig = beanFactory.getDependentBeans(SingletonBeanConfig.class.getName()); + assertThat(dependentsOfSingletonBeanConfig).containsOnly("foo", "bar"); + } + @Test void enhancementIsNotPresentForProxyBeanMethodsFlagSetToFalse() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(NonEnhancedSingletonBeanConfig.class)); @@ -181,7 +197,7 @@ void enhancementIsNotPresentForStaticMethodsUsingAsm() { assertThat(bar.foo).isNotSameAs(foo); } - @Test + @Test // gh-34486 void enhancementIsNotPresentWithEmptyConfig() { beanFactory.registerBeanDefinition("config", new RootBeanDefinition(EmptyConfig.class)); ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); @@ -1195,6 +1211,12 @@ public static Bar bar() { } } + @Configuration + @Import(SingletonBeanConfig.class) + abstract static class AbstractConfigWithoutBeanMethods { + // This class intentionally does NOT declare @Bean methods. + } + @Configuration static final class EmptyConfig { } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java index 5f2fb5f9dcf4..2014c12000b2 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/ImportSelectorTests.java @@ -30,6 +30,7 @@ import java.util.function.Predicate; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -46,7 +47,6 @@ import org.springframework.core.env.Environment; import org.springframework.core.io.ResourceLoader; import org.springframework.core.type.AnnotationMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -403,8 +403,7 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { } @Override - @Nullable - public Predicate getExclusionFilter() { + public @Nullable Predicate getExclusionFilter() { return className -> className.endsWith("ImportedSelector1"); } } @@ -440,18 +439,16 @@ static class GroupedConfig2 { public static class GroupedDeferredImportSelector1 extends DeferredImportSelector1 { - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } } public static class GroupedDeferredImportSelector2 extends DeferredImportSelector2 { - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } } @@ -471,9 +468,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportSelector1.class.getName(), ChildConfiguration1.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } @@ -492,9 +488,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportSelector2.class.getName(), ChildConfiguration2.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } @@ -515,9 +510,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportedSelector3.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } @@ -537,9 +531,8 @@ public String[] selectImports(AnnotationMetadata importingClassMetadata) { return new String[] { DeferredImportSelector2.class.getName() }; } - @Nullable @Override - public Class getImportGroup() { + public @Nullable Class getImportGroup() { return TestImportGroup.class; } diff --git a/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java new file mode 100644 index 000000000000..ac33e4490b6a --- /dev/null +++ b/spring-context/src/test/java/org/springframework/context/annotation/beanregistrar/BeanRegistrarConfigurationTests.java @@ -0,0 +1,96 @@ +/* + * Copyright 2002-2025 the original author 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.context.annotation.beanregistrar; + +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.testfixture.beans.factory.GenericBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Bar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Baz; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Foo; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar.Init; +import org.springframework.context.testfixture.context.annotation.registrar.BeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.GenericBeanRegistrarConfiguration; +import org.springframework.context.testfixture.context.annotation.registrar.ImportAwareBeanRegistrarConfiguration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for {@link BeanRegistrar} imported by @{@link org.springframework.context.annotation.Configuration}. + * + * @author Sebastien Deleuze + */ +public class BeanRegistrarConfigurationTests { + + @Test + void beanRegistrar() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(BeanRegistrarConfiguration.class); + assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class)); + assertThat(context.getBean("foo", Foo.class)).isEqualTo(context.getBean("fooAlias", Foo.class)); + assertThatThrownBy(() -> context.getBean(Baz.class)).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(context.getBean(Init.class).initialized).isTrue(); + BeanDefinition beanDefinition = context.getBeanDefinition("bar"); + assertThat(beanDefinition.getScope()).isEqualTo(BeanDefinition.SCOPE_PROTOTYPE); + assertThat(beanDefinition.isLazyInit()).isTrue(); + assertThat(beanDefinition.getDescription()).isEqualTo("Custom description"); + } + + @Test + void beanRegistrarWithProfile() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(BeanRegistrarConfiguration.class); + context.getEnvironment().addActiveProfile("baz"); + context.refresh(); + assertThat(context.getBean(Baz.class).message()).isEqualTo("Hello World!"); + } + + @Test + void scannedFunctionalConfiguration() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.scan("org.springframework.context.testfixture.context.annotation.registrar"); + context.refresh(); + assertThat(context.getBean(Bar.class).foo()).isEqualTo(context.getBean(Foo.class)); + assertThatThrownBy(() -> context.getBean(Baz.class).message()).isInstanceOf(NoSuchBeanDefinitionException.class); + assertThat(context.getBean(Init.class).initialized).isTrue(); + } + + @Test + void beanRegistrarWithTargetType() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(GenericBeanRegistrarConfiguration.class); + context.refresh(); + RootBeanDefinition beanDefinition = (RootBeanDefinition)context.getBeanDefinition("fooSupplier"); + assertThat(beanDefinition.getResolvableType().resolveGeneric(0)).isEqualTo(GenericBeanRegistrar.Foo.class); + } + + @Test + void beanRegistrarWithImportAware() { + AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); + context.register(ImportAwareBeanRegistrarConfiguration.class); + context.refresh(); + assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()) + .isEqualTo(ImportAwareBeanRegistrarConfiguration.class.getName()); + } + +} 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 2e2ef9173261..a552e26ede17 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 @@ -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. @@ -83,7 +83,7 @@ void customBeanNameIsRespectedWhenConfiguredViaValueAttribute() { () -> ConfigWithBeanWithCustomNameConfiguredViaValueAttribute.testBean, "enigma"); } - private void customBeanNameIsRespected(Class testClass, Supplier testBeanSupplier, String beanName) { + private static void customBeanNameIsRespected(Class testClass, Supplier testBeanSupplier, String beanName) { GenericApplicationContext ac = new GenericApplicationContext(); AnnotationConfigUtils.registerAnnotationConfigProcessors(ac); ac.registerBeanDefinition("config", new RootBeanDefinition(testClass)); @@ -92,8 +92,8 @@ private void customBeanNameIsRespected(Class testClass, Supplier te assertThat(ac.getBean(beanName)).isSameAs(testBeanSupplier.get()); // method name should not be registered - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - ac.getBean("methodName")); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> ac.getBean("methodName")); } @Test @@ -113,11 +113,12 @@ private void aliasesAreRespected(Class testClass, Supplier testBean BeanFactory factory = initBeanFactory(false, testClass); assertThat(factory.getBean(beanName)).isSameAs(testBean); - Arrays.stream(factory.getAliases(beanName)).map(factory::getBean).forEach(alias -> assertThat(alias).isSameAs(testBean)); + assertThat(factory.getAliases(beanName)).extracting(factory::getBean) + .allMatch(alias -> alias == testBean); // method name should not be registered - assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> - factory.getBean("methodName")); + assertThatExceptionOfType(NoSuchBeanDefinitionException.class) + .isThrownBy(() -> factory.getBean("methodName")); } @Test // SPR-11830 @@ -140,8 +141,8 @@ void configWithSetWithProviderImplementation() { @Test void finalBeanMethod() { - assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - initBeanFactory(false, ConfigWithFinalBean.class)); + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> initBeanFactory(false, ConfigWithFinalBean.class)); } @Test @@ -151,8 +152,8 @@ void finalBeanMethodWithoutProxy() { @Test // gh-31007 void voidBeanMethod() { - assertThatExceptionOfType(BeanDefinitionParsingException.class).isThrownBy(() -> - initBeanFactory(false, ConfigWithVoidBean.class)); + assertThatExceptionOfType(BeanDefinitionParsingException.class) + .isThrownBy(() -> initBeanFactory(false, ConfigWithVoidBean.class)); } @Test @@ -180,23 +181,19 @@ void configWithFactoryBeanReturnType() { assertThat(factory.isTypeMatch("&factoryBean", FactoryBean.class)).isTrue(); assertThat(factory.isTypeMatch("&factoryBean", BeanClassLoaderAware.class)).isFalse(); assertThat(factory.isTypeMatch("&factoryBean", ListFactoryBean.class)).isFalse(); - boolean condition = factory.getBean("factoryBean") instanceof List; - assertThat(condition).isTrue(); + assertThat(factory.getBean("factoryBean")).isInstanceOf(List.class); String[] beanNames = factory.getBeanNamesForType(FactoryBean.class); - assertThat(beanNames).hasSize(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertThat(beanNames).containsExactly("&factoryBean"); beanNames = factory.getBeanNamesForType(BeanClassLoaderAware.class); - assertThat(beanNames).hasSize(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertThat(beanNames).containsExactly("&factoryBean"); beanNames = factory.getBeanNamesForType(ListFactoryBean.class); - assertThat(beanNames).hasSize(1); - assertThat(beanNames[0]).isEqualTo("&factoryBean"); + assertThat(beanNames).containsExactly("&factoryBean"); beanNames = factory.getBeanNamesForType(List.class); - assertThat(beanNames[0]).isEqualTo("factoryBean"); + assertThat(beanNames).containsExactly("factoryBean"); } @Test @@ -231,7 +228,7 @@ void configurationWithMethodNameMismatchAndOverridingAllowed() { BeanFactory factory = initBeanFactory(true, ConfigWithMethodNameMismatch.class); SpousyTestBean foo = factory.getBean("foo", SpousyTestBean.class); - assertThat(foo.getName()).isEqualTo("foo1"); + assertThat(foo.getName()).isIn("foo1", "foo2"); } @Test @@ -270,7 +267,7 @@ void configurationWithAdaptiveResourcePrototypes() { 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"); @@ -381,7 +378,7 @@ static class ConfigWithBeanWithCustomName { static TestBean testBean = new TestBean(ConfigWithBeanWithCustomName.class.getSimpleName()); - @Bean(name = "customName") + @Bean("customName") public TestBean methodName() { return testBean; } @@ -405,7 +402,7 @@ static class ConfigWithBeanWithAliases { static TestBean testBean = new TestBean(ConfigWithBeanWithAliases.class.getSimpleName()); - @Bean(name = {"name1", "alias1", "alias2", "alias3"}) + @Bean({"name1", "alias1", "alias2", "alias3"}) public TestBean methodName() { return testBean; } @@ -430,7 +427,7 @@ static class ConfigWithBeanWithProviderImplementation implements Provider set = Collections.singleton("value"); @Override - @Bean(name = "customName") + @Bean("customName") public Set get() { return set; } @@ -453,7 +450,8 @@ public Set get() { @Configuration static class ConfigWithFinalBean { - @Bean public final TestBean testBean() { + @Bean + public final TestBean testBean() { return new TestBean(); } } @@ -462,7 +460,8 @@ static class ConfigWithFinalBean { @Configuration(proxyBeanMethods = false) static class ConfigWithFinalBeanWithoutProxy { - @Bean public final TestBean testBean() { + @Bean + public final TestBean testBean() { return new TestBean(); } } @@ -471,7 +470,8 @@ static class ConfigWithFinalBeanWithoutProxy { @Configuration static class ConfigWithVoidBean { - @Bean public void testBean() { + @Bean + public void testBean() { } } @@ -479,7 +479,8 @@ static class ConfigWithVoidBean { @Configuration static class SimplestPossibleConfig { - @Bean public String stringBean() { + @Bean + public String stringBean() { return "foo"; } } @@ -488,11 +489,13 @@ static class SimplestPossibleConfig { @Configuration static class ConfigWithNonSpecificReturnTypes { - @Bean public Object stringBean() { + @Bean + public Object stringBean() { return "foo"; } - @Bean public FactoryBean factoryBean() { + @Bean + public FactoryBean factoryBean() { ListFactoryBean fb = new ListFactoryBean(); fb.setSourceList(Arrays.asList("element1", "element2")); return fb; @@ -503,29 +506,34 @@ static class ConfigWithNonSpecificReturnTypes { @Configuration static class ConfigWithPrototypeBean { - @Bean public TestBean foo() { + @Bean + public TestBean foo() { TestBean foo = new SpousyTestBean("foo"); foo.setSpouse(bar()); return foo; } - @Bean public TestBean bar() { + @Bean + public TestBean bar() { TestBean bar = new SpousyTestBean("bar"); bar.setSpouse(baz()); return bar; } - @Bean @Scope("prototype") + @Bean + @Scope("prototype") public TestBean baz() { return new TestBean("baz"); } - @Bean @Scope("prototype") + @Bean + @Scope("prototype") public TestBean adaptive1(InjectionPoint ip) { return new TestBean(ip.getMember().getName()); } - @Bean @Scope("prototype") + @Bean + @Scope("prototype") public TestBean adaptive2(DependencyDescriptor dd) { return new TestBean(dd.getMember().getName()); } @@ -542,14 +550,17 @@ public TestBean bar() { } - @Configuration + @SuppressWarnings("deprecation") + @Configuration(enforceUniqueMethods = false) static class ConfigWithMethodNameMismatch { - @Bean(name = "foo") public TestBean foo1() { + @Bean("foo") + public TestBean foo1() { return new SpousyTestBean("foo1"); } - @Bean(name = "foo") public TestBean foo2() { + @Bean("foo") + public TestBean foo2() { return new SpousyTestBean("foo2"); } } @@ -558,12 +569,14 @@ static class ConfigWithMethodNameMismatch { @Scope("prototype") static class AdaptiveInjectionPoints { - @Autowired @Qualifier("adaptive1") + @Autowired + @Qualifier("adaptive1") public TestBean adaptiveInjectionPoint1; public TestBean adaptiveInjectionPoint2; - @Autowired @Qualifier("adaptive2") + @Autowired + @Qualifier("adaptive2") public void setAdaptiveInjectionPoint2(TestBean adaptiveInjectionPoint2) { this.adaptiveInjectionPoint2 = adaptiveInjectionPoint2; } @@ -687,15 +700,16 @@ public ApplicationListener listener() { } + @SuppressWarnings("deprecation") @Configuration(enforceUniqueMethods = false) public static class OverloadedBeanMismatch { - @Bean(name = "other") + @Bean("other") public NestedTestBean foo() { return new NestedTestBean(); } - @Bean(name = "foo") + @Bean("foo") public TestBean foo(@Qualifier("other") NestedTestBean other) { TestBean tb = new TestBean(); tb.setLawyer(other); @@ -728,7 +742,7 @@ static class AbstractPrototype implements PrototypeInterface { static class ConfigWithDynamicPrototype { @Bean - @Scope(value = "prototype") + @Scope("prototype") public PrototypeInterface getDemoBean(int i) { return switch (i) { case 1 -> new PrototypeOne(); 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 565b58e9d805..7b717f2c5af5 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.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 @@ 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 @@ static class ImportXmlWithAopNamespaceConfig { @Aspect static class AnAspect { + @Before("execution(* org.springframework.beans.testfixture.beans.TestBean.*(..))") public void advice() { } } @@ -158,18 +164,37 @@ static class ImportXmlWithConfigurationClass { @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 80172e53808a..3dd4b3561bf2 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 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,29 +54,164 @@ void testProcessImportsWithAsm() { } @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); } + @Test + void importAnnotationWithTwoLevelRecursion() { + int configClasses = 2; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), AppConfig.class); + } + + @Test + void importAnnotationWithThreeLevelRecursion() { + int configClasses = 4; + int beansInClasses = 5; + assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); + } + + @Test + void importAnnotationWithThreeLevelRecursionAndDoubleImport() { + int configClasses = 5; + int beansInClasses = 5; + assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class, FirstLevelPlus.class); + } + + @Test + void importAnnotationWithMultipleArguments() { + int configClasses = 3; + int beansInClasses = 3; + assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); + } + + @Test + void importAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.setAllowBeanDefinitionOverriding(true); + beanFactory.registerBeanDefinition("config", new RootBeanDefinition( + WithMultipleArgumentsThatWillCauseDuplication.class)); + ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); + pp.postProcessBeanFactory(beanFactory); + assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(4); + assertThat(beanFactory.getBean("foo", ITestBean.class).getName()).isEqualTo("foo2"); + } + + @Test + void importAnnotationOnInnerClasses() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), OuterConfig.InnerConfig.class); + } + + @Test + void importNonConfigurationAnnotationClass() { + int configClasses = 2; + int beansInClasses = 0; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigAnnotated.class); + } + + /** + * 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 + * or in the case of automatic registration via nesting + */ + @Test + void reproSpr9023() { + 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(); + } + + @Test + void processImports() { + int configClasses = 2; + int beansInClasses = 2; + assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); + } + + /** + * An imported config must override a scanned one, thus bean definitions + * from the imported class is overridden by its importer. + */ + @Test // gh-24643 + void importedConfigOverridesScanned() { + AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); + ctx.setAllowBeanDefinitionOverriding(true); + ctx.scan(SiblingImportingConfigA.class.getPackage().getName()); + ctx.refresh(); + + assertThat(ctx.getBean("a-imports-b")).isEqualTo("valueFromA"); + assertThat(ctx.getBean("b-imports-a")).isEqualTo("valueFromBR"); + assertThat(ctx.getBeansOfType(SiblingImportingConfigA.class)).hasSize(1); + 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(); @@ -107,6 +221,7 @@ ITestBean one() { @Configuration @Import(OtherConfiguration.class) static class OtherConfigurationWithImportAnnotation { + @Bean ITestBean two() { return new TestBean(); @@ -115,21 +230,13 @@ ITestBean two() { @Configuration static class OtherConfiguration { + @Bean ITestBean three() { return new TestBean(); } } - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationWithTwoLevelRecursion() { - int configClasses = 2; - int beansInClasses = 3; - assertBeanDefinitionCount((configClasses + beansInClasses), AppConfig.class); - } - @Configuration @Import(DataSourceConfig.class) static class AppConfig { @@ -147,49 +254,13 @@ ITestBean accountRepository() { @Configuration static class DataSourceConfig { + @Bean ITestBean dataSourceA() { return new TestBean(); } } - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationWithThreeLevelRecursion() { - int configClasses = 4; - int beansInClasses = 5; - assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class); - } - - @Test - void testImportAnnotationWithThreeLevelRecursionAndDoubleImport() { - int configClasses = 5; - int beansInClasses = 5; - assertBeanDefinitionCount(configClasses + beansInClasses, FirstLevel.class, FirstLevelPlus.class); - } - - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationWithMultipleArguments() { - int configClasses = 3; - int beansInClasses = 3; - assertBeanDefinitionCount((configClasses + beansInClasses), WithMultipleArgumentsToImportAnnotation.class); - } - - @Test - void testImportAnnotationWithMultipleArgumentsResultingInOverriddenBeanDefinition() { - DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); - beanFactory.setAllowBeanDefinitionOverriding(true); - beanFactory.registerBeanDefinition("config", new RootBeanDefinition( - WithMultipleArgumentsThatWillCauseDuplication.class)); - ConfigurationClassPostProcessor pp = new ConfigurationClassPostProcessor(); - pp.postProcessBeanFactory(beanFactory); - assertThat(beanFactory.getBeanDefinitionCount()).isEqualTo(4); - assertThat(beanFactory.getBean("foo", ITestBean.class).getName()).isEqualTo("foo2"); - } - @Configuration @Import({Foo1.class, Foo2.class}) static class WithMultipleArgumentsThatWillCauseDuplication { @@ -197,6 +268,7 @@ static class WithMultipleArgumentsThatWillCauseDuplication { @Configuration static class Foo1 { + @Bean ITestBean foo() { return new TestBean("foo1"); @@ -205,23 +277,16 @@ ITestBean foo() { @Configuration static class Foo2 { + @Bean ITestBean foo() { return new TestBean("foo2"); } } - // ------------------------------------------------------------------------ - - @Test - void testImportAnnotationOnInnerClasses() { - int configClasses = 2; - int beansInClasses = 2; - assertBeanDefinitionCount((configClasses + beansInClasses), OuterConfig.InnerConfig.class); - } - @Configuration static class OuterConfig { + @Bean String whatev() { return "whatev"; @@ -239,17 +304,17 @@ ITestBean innerBean() { @Configuration static class ExternalConfig { + @Bean ITestBean extBean() { return new TestBean(); } } - // ------------------------------------------------------------------------ - @Configuration @Import(SecondLevel.class) static class FirstLevel { + @Bean TestBean m() { return new TestBean(); @@ -264,6 +329,7 @@ static class FirstLevelPlus { @Configuration @Import({ThirdLevel.class, InitBean.class}) static class SecondLevel { + @Bean TestBean n() { return new TestBean(); @@ -273,6 +339,7 @@ TestBean n() { @Configuration @DependsOn("org.springframework.context.annotation.configuration.ImportTests$InitBean") static class ThirdLevel { + ThirdLevel() { assertThat(InitBean.initialized).isTrue(); } @@ -294,7 +361,8 @@ ITestBean thirdLevelC() { } static class InitBean { - public static boolean initialized = false; + + static boolean initialized = false; InitBean() { initialized = true; @@ -304,6 +372,7 @@ static class InitBean { @Configuration @Import({LeftConfig.class, RightConfig.class}) static class WithMultipleArgumentsToImportAnnotation { + @Bean TestBean m() { return new TestBean(); @@ -312,6 +381,7 @@ TestBean m() { @Configuration static class LeftConfig { + @Bean ITestBean left() { return new TestBean(); @@ -320,44 +390,19 @@ ITestBean left() { @Configuration static class RightConfig { + @Bean ITestBean right() { return new TestBean(); } } - // ------------------------------------------------------------------------ - - @Test - void testImportNonConfigurationAnnotationClass() { - 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 - * or in the case of automatic registration via nesting - */ - @Test - void reproSpr9023() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.register(B.class); - ctx.refresh(); - 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 { } @@ -365,30 +410,38 @@ static class A { } @Import(A.class) static class B { } - // ------------------------------------------------------------------------ + record ImportedBean(String name) { + } - @Test - void testProcessImports() { - int configClasses = 2; - int beansInClasses = 2; - assertBeanDefinitionCount((configClasses + beansInClasses), ConfigurationWithImportAnnotation.class); + @Configuration + static class ImportedConfig { + + @Bean + ImportedBean importedBean() { + return new ImportedBean("imported"); + } } - /** - * An imported config must override a scanned one, thus bean definitions - * from the imported class is overridden by its importer. - */ - @Test // gh-24643 - void importedConfigOverridesScanned() { - AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(); - ctx.setAllowBeanDefinitionOverriding(true); - ctx.scan(SiblingImportingConfigA.class.getPackage().getName()); - ctx.refresh(); + @Configuration + static class OverridingImportedConfig { - assertThat(ctx.getBean("a-imports-b")).isEqualTo("valueFromA"); - assertThat(ctx.getBean("b-imports-a")).isEqualTo("valueFromBR"); - assertThat(ctx.getBeansOfType(SiblingImportingConfigA.class)).hasSize(1); - assertThat(ctx.getBeansOfType(SiblingImportingConfigB.class)).hasSize(1); + @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 f9d0574d552a..bc36d8fd46f6 100644 --- a/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java +++ b/spring-context/src/test/java/org/springframework/context/annotation/jsr330/SpringAtInjectTckTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -38,7 +38,8 @@ * @author Juergen Hoeller * @since 3.0 */ -class SpringAtInjectTckTests { +// WARNING: This class MUST be public, since it is based on JUnit 3. +public class SpringAtInjectTckTests { @SuppressWarnings("unchecked") public static Test suite() { diff --git a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java index b2da6f1c7bac..e1cffd8c7e3c 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ApplicationContextAotGeneratorTests.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,7 +17,6 @@ package org.springframework.context.aot; import java.io.IOException; -import java.lang.reflect.Constructor; import java.lang.reflect.Proxy; import java.util.List; import java.util.function.BiConsumer; @@ -565,8 +564,7 @@ void processAheadOfTimeWhenHasCglibProxyWithArgumentsRegisterIntrospectionHintsO GenericApplicationContext applicationContext = new AnnotationConfigApplicationContext(); applicationContext.registerBean(ConfigurableCglibConfiguration.class); TestGenerationContext generationContext = processAheadOfTime(applicationContext); - Constructor userConstructor = ConfigurableCglibConfiguration.class.getDeclaredConstructors()[0]; - assertThat(RuntimeHintsPredicates.reflection().onConstructor(userConstructor).introspect()) + assertThat(RuntimeHintsPredicates.reflection().onType(ConfigurableCglibConfiguration.class)) .accepts(generationContext.getRuntimeHints()); } 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 384f54d59fcc..aee1446448ab 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-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. @@ -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 @@ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException 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 @@ void processingDeletesExistingOutput(@TempDir Path directory) throws IOException 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(); @@ -102,7 +105,7 @@ private Consumer hasGeneratedAssetsForSampleApplication() { assertThat(directory.resolve( "source/org/springframework/context/aot/ContextAotProcessorTests_SampleApplication__BeanFactoryRegistrations.java")) .exists().isRegularFile(); - assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/reflect-config.json")) + assertThat(directory.resolve("resource/META-INF/native-image/com.example/example/reachability-metadata.json")) .exists().isRegularFile(); Path nativeImagePropertiesFile = directory .resolve("resource/META-INF/native-image/com.example/example/native-image.properties"); @@ -118,6 +121,8 @@ private Consumer hasGeneratedAssetsForSampleApplication() { 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 @@ private static Settings createSettings(Path sourceOutput, Path resourceOutput, 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 @@ static class SampleApplication { public String testBean() { return "Hello"; } - } } diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java index 5e08fafbf6ba..2e2dc67b2b4d 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorAotContributionBuilderTests.java @@ -20,6 +20,7 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.api.ObjectArrayAssert; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; @@ -35,7 +36,6 @@ import org.springframework.context.testfixture.context.aot.scan.reflective2.Reflective2OnType; import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective21.Reflective21OnType; import org.springframework.context.testfixture.context.aot.scan.reflective2.reflective22.Reflective22OnType; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java index dedaa501ce16..22f61e1c4f9f 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/ReflectiveProcessorBeanFactoryInitializationAotProcessorTests.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. @@ -16,8 +16,6 @@ package org.springframework.context.aot; -import java.lang.reflect.Constructor; - import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GenerationContext; @@ -69,8 +67,7 @@ void shouldProcessAnnotationOnType() { void shouldProcessAllBeans() throws NoSuchMethodException { ReflectionHintsPredicates reflection = RuntimeHintsPredicates.reflection(); process(SampleTypeAnnotatedBean.class, SampleConstructorAnnotatedBean.class); - Constructor constructor = SampleConstructorAnnotatedBean.class.getDeclaredConstructor(String.class); - assertThat(reflection.onType(SampleTypeAnnotatedBean.class).and(reflection.onConstructor(constructor))) + assertThat(reflection.onType(SampleTypeAnnotatedBean.class)) .accepts(this.generationContext.getRuntimeHints()); } diff --git a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java index bb4aa59ff8b1..ecf4d6f22a02 100644 --- a/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/context/aot/RuntimeHintsBeanFactoryInitializationAotProcessorTests.java @@ -22,6 +22,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -38,7 +39,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.ImportRuntimeHints; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; diff --git a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java index ac511516963f..5578213184e8 100644 --- a/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java +++ b/spring-context/src/test/java/org/springframework/context/event/AnnotationDrivenEventListenerTests.java @@ -279,25 +279,6 @@ void collectionReplyNullValue() { this.eventCollector.assertTotalEventsCount(2); } - @Test - @SuppressWarnings({"deprecation", "removal"}) - void listenableFutureReply() { - load(TestEventListener.class, ReplyEventListener.class); - org.springframework.util.concurrent.SettableListenableFuture future = - new org.springframework.util.concurrent.SettableListenableFuture<>(); - future.set("dummy"); - AnotherTestEvent event = new AnotherTestEvent(this, future); - ReplyEventListener replyEventListener = this.context.getBean(ReplyEventListener.class); - TestEventListener listener = this.context.getBean(TestEventListener.class); - - this.eventCollector.assertNoEventReceived(listener); - this.eventCollector.assertNoEventReceived(replyEventListener); - this.context.publishEvent(event); - this.eventCollector.assertEvent(replyEventListener, event); - this.eventCollector.assertEvent(listener, "dummy"); // reply - this.eventCollector.assertTotalEventsCount(2); - } - @Test void completableFutureReply() { load(TestEventListener.class, ReplyEventListener.class); diff --git a/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java b/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java index 80add57c4d2d..4b3773c7ca79 100644 --- a/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java +++ b/spring-context/src/test/java/org/springframework/context/event/test/AbstractIdentifiable.java @@ -18,7 +18,7 @@ import java.util.UUID; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Stephane Nicoll diff --git a/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java b/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java index 51f06ee24fd3..bbff12e68667 100644 --- a/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java +++ b/spring-context/src/test/java/org/springframework/context/event/test/GenericEventPojo.java @@ -16,9 +16,10 @@ package org.springframework.context.event.test; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.ResolvableTypeProvider; -import org.springframework.lang.Nullable; /** * A simple POJO that implements {@link ResolvableTypeProvider}. diff --git a/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java b/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java index b4e5d98ffbe6..7df44c59232a 100644 --- a/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java +++ b/spring-context/src/test/java/org/springframework/context/event/test/IdentifiableApplicationEvent.java @@ -18,8 +18,9 @@ import java.util.UUID; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; -import org.springframework.lang.Nullable; /** * A basic test event that can be uniquely identified easily. 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 4e18f567b6c1..347aa9171de9 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 @@ 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/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java b/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java index f15f3589ee7a..4ab27ca122dc 100644 --- a/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java +++ b/spring-context/src/test/java/org/springframework/context/generator/ApplicationContextAotGeneratorRuntimeHintsTests.java @@ -24,7 +24,6 @@ import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.test.agent.EnabledIfRuntimeHintsAgent; import org.springframework.aot.test.agent.RuntimeHintsInvocations; -import org.springframework.aot.test.agent.RuntimeHintsRecorder; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.RootBeanDefinition; @@ -46,6 +45,7 @@ * @author Stephane Nicoll */ @EnabledIfRuntimeHintsAgent +@SuppressWarnings("removal") class ApplicationContextAotGeneratorRuntimeHintsTests { @Test @@ -100,7 +100,7 @@ private void compile(GenericApplicationContext applicationContext, TestCompiler.forSystem().with(generationContext).compile(compiled -> { ApplicationContextInitializer instance = compiled.getInstance(ApplicationContextInitializer.class); GenericApplicationContext freshContext = new GenericApplicationContext(); - RuntimeHintsInvocations recordedInvocations = RuntimeHintsRecorder.record(() -> { + RuntimeHintsInvocations recordedInvocations = org.springframework.aot.test.agent.RuntimeHintsRecorder.record(() -> { instance.initialize(freshContext); freshContext.refresh(); freshContext.close(); 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 7c153e3fd42c..b1cb1b8aa304 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 @@ void definedBeanFactoryPostProcessor() { } @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/ConversionServiceFactoryBeanTests.java b/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java index fc737bbfa317..4f7d7f295c9a 100644 --- a/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/ConversionServiceFactoryBeanTests.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.context.ConfigurableApplicationContext; @@ -31,7 +32,6 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.FileSystemResource; -import org.springframework.lang.Nullable; import org.springframework.tests.sample.beans.ResourceTestBean; import static org.assertj.core.api.Assertions.assertThat; @@ -82,8 +82,7 @@ public Set getConvertibleTypes() { return Collections.singleton(new ConvertiblePair(String.class, Baz.class)); } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return new Baz(); } }); diff --git a/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java b/spring-context/src/test/java/org/springframework/context/support/DefaultLifecycleProcessorTests.java index da666fea6ec4..1a657a7f7f7f 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.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 @@ void defaultLifecycleProcessorInstance() { @Test void customLifecycleProcessorInstance() { + StaticApplicationContext context = new StaticApplicationContext(); BeanDefinition beanDefinition = new RootBeanDefinition(DefaultLifecycleProcessor.class); beanDefinition.getPropertyValues().addPropertyValue("timeoutPerShutdownPhase", 1000); - StaticApplicationContext context = new StaticApplicationContext(); - context.registerBeanDefinition("lifecycleProcessor", beanDefinition); + context.registerBeanDefinition(StaticApplicationContext.LIFECYCLE_PROCESSOR_BEAN_NAME, beanDefinition); + context.refresh(); LifecycleProcessor bean = context.getBean("lifecycleProcessor", LifecycleProcessor.class); Object contextLifecycleProcessor = new DirectFieldAccessor(context).getPropertyValue("lifecycleProcessor"); @@ -70,11 +73,12 @@ void customLifecycleProcessorInstance() { @Test void singleSmartLifecycleAutoStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); context.refresh(); assertThat(bean.isRunning()).isTrue(); @@ -114,12 +118,13 @@ void singleSmartLifecycleAutoStartupWithLazyInitFactoryBean() { @Test void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.registerSingleton("failingBean", FailingLifecycleBean.class); + assertThat(bean.isRunning()).isFalse(); assertThatExceptionOfType(ApplicationContextException.class) .isThrownBy(context::refresh).withCauseInstanceOf(IllegalStateException.class); @@ -130,11 +135,12 @@ void singleSmartLifecycleAutoStartupWithFailingLifecycleBean() { @Test void singleSmartLifecycleWithoutAutoStartup() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(false); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + assertThat(bean.isRunning()).isFalse(); context.refresh(); assertThat(bean.isRunning()).isFalse(); @@ -148,15 +154,16 @@ void singleSmartLifecycleWithoutAutoStartup() { @Test void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forStartupTests(1, startedBeans); bean.setAutoStartup(true); TestSmartLifecycleBean dependency = TestSmartLifecycleBean.forStartupTests(1, startedBeans); dependency.setAutoStartup(false); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.getBeanFactory().registerSingleton("dependency", dependency); context.getBeanFactory().registerDependentBean("dependency", "bean"); + assertThat(bean.isRunning()).isFalse(); assertThat(dependency.isRunning()).isFalse(); context.refresh(); @@ -169,20 +176,42 @@ void singleSmartLifecycleAutoStartupWithNonAutoStartupDependency() { 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 @@ void smartLifecycleGroupStartup() { @Test void contextRefreshThenStartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -233,16 +263,17 @@ void contextRefreshThenStartWithMixedBeans() { @Test void contextRefreshThenStopAndRestartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -270,16 +301,17 @@ void contextRefreshThenStopAndRestartWithMixedBeans() { @Test void contextRefreshThenStopForRestartWithMixedBeans() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestLifecycleBean simpleBean1 = TestLifecycleBean.forStartupTests(startedBeans); TestLifecycleBean simpleBean2 = TestLifecycleBean.forStartupTests(startedBeans); TestSmartLifecycleBean smartBean1 = TestSmartLifecycleBean.forStartupTests(5, startedBeans); TestSmartLifecycleBean smartBean2 = TestSmartLifecycleBean.forStartupTests(-3, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("simpleBean1", simpleBean1); context.getBeanFactory().registerSingleton("smartBean1", smartBean1); context.getBeanFactory().registerSingleton("simpleBean2", simpleBean2); context.getBeanFactory().registerSingleton("smartBean2", smartBean2); + assertThat(simpleBean1.isRunning()).isFalse(); assertThat(simpleBean2.isRunning()).isFalse(); assertThat(smartBean1.isRunning()).isFalse(); @@ -319,6 +351,7 @@ void contextRefreshThenStopForRestartWithMixedBeans() { @Test @EnabledForTestGroups(LONG_RUNNING) void smartLifecycleGroupShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 300, stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(3, 100, stoppedBeans); @@ -327,7 +360,6 @@ void smartLifecycleGroupShutdown() { TestSmartLifecycleBean bean5 = TestSmartLifecycleBean.forShutdownTests(2, 700, stoppedBeans); TestSmartLifecycleBean bean6 = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 200, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(3, 200, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean3", bean3); @@ -335,6 +367,7 @@ void smartLifecycleGroupShutdown() { context.getBeanFactory().registerSingleton("bean5", bean5); context.getBeanFactory().registerSingleton("bean6", bean6); context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); context.stop(); assertThat(stoppedBeans).satisfiesExactly(hasPhase(Integer.MAX_VALUE), hasPhase(3), @@ -345,11 +378,12 @@ void smartLifecycleGroupShutdown() { @Test @EnabledForTestGroups(LONG_RUNNING) void singleSmartLifecycleShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean = TestSmartLifecycleBean.forShutdownTests(99, 300, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); context.refresh(); + assertThat(bean.isRunning()).isTrue(); context.stop(); assertThat(bean.isRunning()).isFalse(); @@ -359,10 +393,11 @@ void singleSmartLifecycleShutdown() { @Test void singleLifecycleShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean = new TestLifecycleBean(null, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean", bean); + context.refresh(); assertThat(bean.isRunning()).isFalse(); bean.start(); @@ -375,6 +410,7 @@ void singleLifecycleShutdown() { @Test void mixedShutdown() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); Lifecycle bean1 = TestLifecycleBean.forShutdownTests(stoppedBeans); Lifecycle bean2 = TestSmartLifecycleBean.forShutdownTests(500, 200, stoppedBeans); @@ -383,7 +419,6 @@ void mixedShutdown() { Lifecycle bean5 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); Lifecycle bean6 = TestSmartLifecycleBean.forShutdownTests(-1, 100, stoppedBeans); Lifecycle bean7 = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 300, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean3", bean3); @@ -391,6 +426,7 @@ void mixedShutdown() { context.getBeanFactory().registerSingleton("bean5", bean5); context.getBeanFactory().registerSingleton("bean6", bean6); context.getBeanFactory().registerSingleton("bean7", bean7); + context.refresh(); assertThat(bean2.isRunning()).isTrue(); assertThat(bean3.isRunning()).isTrue(); @@ -418,17 +454,18 @@ void mixedShutdown() { @Test void dependencyStartedFirstEvenIfItsPhaseIsHigher() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forStartupTests(2, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forStartupTests(Integer.MAX_VALUE, startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean2.isRunning()).isTrue(); @@ -446,6 +483,7 @@ void dependencyStartedFirstEvenIfItsPhaseIsHigher() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstEvenIfItsPhaseIsLower() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 100, stoppedBeans); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); @@ -453,7 +491,6 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestSmartLifecycleBean beanMax = TestSmartLifecycleBean.forShutdownTests(Integer.MAX_VALUE, 400, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); @@ -461,6 +498,7 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("beanMax", beanMax); context.getBeanFactory().registerDependentBean("bean99", "bean2"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean1.isRunning()).isTrue(); @@ -486,17 +524,18 @@ void dependentShutdownFirstEvenIfItsPhaseIsLower() { @Test void dependencyStartedFirstAndIsSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forStartupTests(-99, startedBeans); TestSmartLifecycleBean bean99 = TestSmartLifecycleBean.forStartupTests(99, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanNegative", beanNegative); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("bean99", bean99); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("bean7", "simpleBean"); + context.refresh(); context.stop(); startedBeans.clear(); @@ -514,6 +553,7 @@ void dependencyStartedFirstAndIsSmartLifecycle() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstAndIsSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); TestSmartLifecycleBean beanNegative = TestSmartLifecycleBean.forShutdownTests(-99, 100, stoppedBeans); @@ -521,7 +561,6 @@ void dependentShutdownFirstAndIsSmartLifecycle() { TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("beanNegative", beanNegative); context.getBeanFactory().registerSingleton("bean1", bean1); @@ -529,6 +568,7 @@ void dependentShutdownFirstAndIsSmartLifecycle() { context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("simpleBean", "beanNegative"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(beanNegative.isRunning()).isTrue(); @@ -551,15 +591,16 @@ void dependentShutdownFirstAndIsSmartLifecycle() { @Test void dependencyStartedFirstButNotSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList startedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forStartupTests(Integer.MIN_VALUE, startedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forStartupTests(7, startedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forStartupTests(startedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("simpleBean", "beanMin"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean7.isRunning()).isTrue(); @@ -572,19 +613,20 @@ void dependencyStartedFirstButNotSmartLifecycle() { @Test @EnabledForTestGroups(LONG_RUNNING) void dependentShutdownFirstButNotSmartLifecycle() { + StaticApplicationContext context = new StaticApplicationContext(); CopyOnWriteArrayList stoppedBeans = new CopyOnWriteArrayList<>(); TestSmartLifecycleBean bean1 = TestSmartLifecycleBean.forShutdownTests(1, 200, stoppedBeans); TestLifecycleBean simpleBean = TestLifecycleBean.forShutdownTests(stoppedBeans); TestSmartLifecycleBean bean2 = TestSmartLifecycleBean.forShutdownTests(2, 300, stoppedBeans); TestSmartLifecycleBean bean7 = TestSmartLifecycleBean.forShutdownTests(7, 400, stoppedBeans); TestSmartLifecycleBean beanMin = TestSmartLifecycleBean.forShutdownTests(Integer.MIN_VALUE, 400, stoppedBeans); - StaticApplicationContext context = new StaticApplicationContext(); context.getBeanFactory().registerSingleton("beanMin", beanMin); context.getBeanFactory().registerSingleton("bean1", bean1); context.getBeanFactory().registerSingleton("bean2", bean2); context.getBeanFactory().registerSingleton("bean7", bean7); context.getBeanFactory().registerSingleton("simpleBean", simpleBean); context.getBeanFactory().registerDependentBean("bean2", "simpleBean"); + context.refresh(); assertThat(beanMin.isRunning()).isTrue(); assertThat(bean1.isRunning()).isTrue(); @@ -611,6 +653,7 @@ private Consumer hasPhase(int phase) { }; } + private static class TestLifecycleBean implements Lifecycle { private final CopyOnWriteArrayList startedBeans; diff --git a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java index c96b05b3b1a1..b89874308324 100644 --- a/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/GenericApplicationContextTests.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,6 +46,8 @@ import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar; import org.springframework.core.DecoratingProxy; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; @@ -627,6 +629,22 @@ public Class determineBeanType(Class beanClass, String beanName) throws Be context.close(); } + @Test + void beanRegistrar() { + GenericApplicationContext context = new GenericApplicationContext(); + context.register(new SampleBeanRegistrar()); + context.refresh(); + assertThat(context.getBean(SampleBeanRegistrar.Bar.class).foo()).isEqualTo(context.getBean(SampleBeanRegistrar.Foo.class)); + } + + @Test + void importAwareBeanRegistrar() { + GenericApplicationContext context = new GenericApplicationContext(); + context.register(new ImportAwareBeanRegistrar()); + context.refresh(); + assertThat(context.getBean(ImportAwareBeanRegistrar.ClassNameHolder.class).className()).isNull(); + } + private MergedBeanDefinitionPostProcessor registerMockMergedBeanDefinitionPostProcessor(GenericApplicationContext context) { MergedBeanDefinitionPostProcessor bpp = mock(); 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 5df9f1c089c9..4efbe86f28f7 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 @@ * @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 c8bb5d5ef708..71c9eeea73bd 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.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 @@ void replacementFromEnvironmentProperties() { 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(); @@ -88,14 +176,29 @@ void localPropertiesViaResource() { 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 @@ public Object getProperty(String key) { 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 @@ void optionalPropertyWithoutValue() { } + /** + * 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 @@ static PropertySourcesPlaceholderConfigurer pspc() { } } + @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/context/support/StaticApplicationContextMulticasterTests.java b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java index 62535c0d2724..2f1d47fc2c1c 100644 --- a/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java +++ b/spring-context/src/test/java/org/springframework/context/support/StaticApplicationContextMulticasterTests.java @@ -20,6 +20,7 @@ import java.util.Locale; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.MutablePropertyValues; @@ -34,7 +35,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; 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 c57bc66bac59..bb6d42b4874c 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.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 @@ private static class RandomInstantProvider implements ArgumentsProvider { 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 @@ private static final class RandomEpochMillisProvider implements ArgumentsProvide 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/jmx/export/TestDynamicMBean.java b/spring-context/src/test/java/org/springframework/jmx/export/TestDynamicMBean.java index 819dc5af86e6..1878c0aee731 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/TestDynamicMBean.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/TestDynamicMBean.java @@ -25,7 +25,7 @@ import javax.management.MBeanNotificationInfo; import javax.management.MBeanOperationInfo; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Rob Harrop diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java index 8adecbd8d86b..8949080954f2 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationMetadataAssemblerTests.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.jmx.export.annotation; +import javax.management.MBeanNotificationInfo; import javax.management.modelmbean.ModelMBeanAttributeInfo; import javax.management.modelmbean.ModelMBeanInfo; import javax.management.modelmbean.ModelMBeanOperationInfo; @@ -36,6 +37,17 @@ class AnnotationMetadataAssemblerTests extends AbstractMetadataAssemblerTests { private static final String OBJECT_NAME = "bean:name=testBean4"; + @Test + @Override + protected void notificationMetadata() throws Exception { + ModelMBeanInfo info = (ModelMBeanInfo) getMBeanInfo(); + MBeanNotificationInfo[] notifications = info.getNotifications(); + assertThat(notifications).as("Incorrect number of notifications").hasSize(2); + assertThat(notifications[0].getName()).as("Incorrect notification name").isEqualTo("My Notification 1"); + assertThat(notifications[0].getNotifTypes()).as("notification types").containsExactly("type.foo", "type.bar"); + assertThat(notifications[1].getName()).as("Incorrect notification name").isEqualTo("My Notification 2"); + assertThat(notifications[1].getNotifTypes()).as("notification types").containsExactly("type.enigma"); + } @Test void testAttributeFromInterface() throws Exception { @@ -111,4 +123,5 @@ protected int getExpectedAttributeCount() { protected int getExpectedOperationCount() { return super.getExpectedOperationCount() + 4; } + } diff --git a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.java b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.java index d3a5ddf368b8..2303af9af87f 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/annotation/AnnotationTestBean.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,10 +25,11 @@ * @author Juergen Hoeller */ @Service("testBean") -@ManagedResource(objectName = "bean:name=testBean4", description = "My Managed Bean", log = true, +@ManagedResource(value = "bean:name=testBean4", description = "My Managed Bean", log = true, logFile = "build/jmx.log", currencyTimeLimit = 15, persistPolicy = "OnUpdate", persistPeriod = 200, persistLocation = "./foo", persistName = "bar.jmx") -@ManagedNotification(name = "My Notification", notificationTypes = { "type.foo", "type.bar" }) +@ManagedNotification(name = "My Notification 1", notificationTypes = { "type.foo", "type.bar" }) +@ManagedNotification(name = "My Notification 2", notificationTypes = "type.enigma") public class AnnotationTestBean implements ITestBean { private String name; diff --git a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java index 2da809cbcd89..b7c96739f3c3 100644 --- a/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.java +++ b/spring-context/src/test/java/org/springframework/jmx/export/assembler/AbstractJmxAssemblerTests.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. @@ -150,7 +150,7 @@ void attributeHasCorrespondingOperations() throws Exception { } @Test - void notificationMetadata() throws Exception { + protected void notificationMetadata() throws Exception { ModelMBeanInfo info = (ModelMBeanInfo) getMBeanInfo(); MBeanNotificationInfo[] notifications = info.getNotifications(); assertThat(notifications).as("Incorrect number of notifications").hasSize(1); diff --git a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java index 9a533f563570..34cde9f63485 100644 --- a/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java +++ b/spring-context/src/test/java/org/springframework/mock/env/MockEnvironment.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -27,7 +27,7 @@ * @author Chris Beams * @author Sam Brannen * @since 3.2 - * @see org.springframework.core.testfixture.env.MockPropertySource + * @see MockPropertySource */ public class MockEnvironment extends AbstractEnvironment { @@ -44,19 +44,23 @@ public MockEnvironment() { /** * 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/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java index 3d73ac0bea83..42d5851eec2d 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncAnnotationBeanPostProcessorTests.java @@ -200,20 +200,6 @@ public void handleExceptionWithFuture() { assertFutureWithException(result, exceptionHandler); } - @Test - @SuppressWarnings("resource") - public void handleExceptionWithListenableFuture() { - ConfigurableApplicationContext context = - new AnnotationConfigApplicationContext(ConfigWithExceptionHandler.class); - ITestBean testBean = context.getBean("target", ITestBean.class); - - TestableAsyncUncaughtExceptionHandler exceptionHandler = - context.getBean("exceptionHandler", TestableAsyncUncaughtExceptionHandler.class); - assertThat(exceptionHandler.isCalled()).as("handler should not have been called yet").isFalse(); - Future result = testBean.failWithListenableFuture(); - assertFutureWithException(result, exceptionHandler); - } - private void assertFutureWithException(Future result, TestableAsyncUncaughtExceptionHandler exceptionHandler) { assertThatExceptionOfType(ExecutionException.class).isThrownBy( @@ -275,9 +261,6 @@ private interface ITestBean { Future failWithFuture(); - @SuppressWarnings({"deprecation", "removal"}) - org.springframework.util.concurrent.ListenableFuture failWithListenableFuture(); - void failWithVoid(); void await(long timeout); @@ -308,13 +291,6 @@ public Future failWithFuture() { throw new UnsupportedOperationException("failWithFuture"); } - @Async - @Override - @SuppressWarnings({"deprecation", "removal"}) - public org.springframework.util.concurrent.ListenableFuture failWithListenableFuture() { - throw new UnsupportedOperationException("failWithListenableFuture"); - } - @Async @Override public void failWithVoid() { diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java index 5be5a1214b58..470ec3af0940 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncExecutionTests.java @@ -42,7 +42,6 @@ import org.springframework.context.ApplicationListener; import org.springframework.context.support.GenericApplicationContext; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; -import org.springframework.util.concurrent.ListenableFuture; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -51,7 +50,6 @@ * @author Juergen Hoeller * @author Chris Beams */ -@SuppressWarnings({"resource", "deprecation", "removal"}) class AsyncExecutionTests { private static String originalThreadName; @@ -81,30 +79,20 @@ void asyncMethods() throws Exception { asyncTest.doSomething(10); Future future = asyncTest.returnSomething(20); assertThat(future.get()).isEqualTo("20"); - ListenableFuture listenableFuture = asyncTest.returnSomethingListenable(20); - assertThat(listenableFuture.get()).isEqualTo("20"); CompletableFuture completableFuture = asyncTest.returnSomethingCompletable(20); assertThat(completableFuture.get()).isEqualTo("20"); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomething(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomething(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomething(-1).get()) - .withCauseInstanceOf(IOException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomething(-1).get()) + .withCauseInstanceOf(IOException.class); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingListenable(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingListenable(-1).get()) - .withCauseInstanceOf(IOException.class); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingCompletable(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomethingCompletable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -174,22 +162,16 @@ void asyncClass() throws Exception { asyncTest.doSomething(10); Future future = asyncTest.returnSomething(20); assertThat(future.get()).isEqualTo("20"); - ListenableFuture listenableFuture = asyncTest.returnSomethingListenable(20); - assertThat(listenableFuture.get()).isEqualTo("20"); CompletableFuture completableFuture = asyncTest.returnSomethingCompletable(20); assertThat(completableFuture.get()).isEqualTo("20"); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomething(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingListenable(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomething(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); - assertThatExceptionOfType(ExecutionException.class).isThrownBy(() -> - asyncTest.returnSomethingCompletable(0).get()) - .withCauseInstanceOf(IllegalArgumentException.class); + assertThatExceptionOfType(ExecutionException.class) + .isThrownBy(() -> asyncTest.returnSomethingCompletable(0).get()) + .withCauseInstanceOf(IllegalArgumentException.class); } @Test @@ -408,6 +390,7 @@ public void doSomething(int i) { } @Async + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); if (i == 0) { @@ -419,18 +402,6 @@ else if (i < 0) { return AsyncResult.forValue(Integer.toString(i)); } - @Async - public ListenableFuture returnSomethingListenable(int i) { - assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); - if (i == 0) { - throw new IllegalArgumentException(); - } - else if (i < 0) { - return AsyncResult.forExecutionException(new IOException()); - } - return new AsyncResult<>(Integer.toString(i)); - } - @Async public CompletableFuture returnSomethingCompletable(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); @@ -465,12 +436,14 @@ public void doSomething(int i) { } @MyAsync + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); assertThat(Thread.currentThread().getName()).startsWith("e2-"); return new AsyncResult<>(Integer.toString(i)); } + @SuppressWarnings("deprecation") public Future returnSomething2(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); assertThat(Thread.currentThread().getName()).startsWith("e0-"); @@ -497,6 +470,7 @@ public void doSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); } + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); if (i == 0) { @@ -505,14 +479,6 @@ public Future returnSomething(int i) { return new AsyncResult<>(Integer.toString(i)); } - public ListenableFuture returnSomethingListenable(int i) { - assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); - if (i == 0) { - throw new IllegalArgumentException(); - } - return new AsyncResult<>(Integer.toString(i)); - } - @Async public CompletableFuture returnSomethingCompletable(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); @@ -545,6 +511,7 @@ public void doSomething(int i) { } @Override + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); return new AsyncResult<>(Integer.toString(i)); @@ -569,6 +536,7 @@ public void doSomething(int i) { } @Override + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); return new AsyncResult<>(Integer.toString(i)); @@ -580,6 +548,7 @@ public static class DynamicAsyncInterfaceBean implements FactoryBean()); DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor((MethodInterceptor) invocation -> { @@ -636,6 +605,7 @@ public void doSomething(int i) { } @Override + @SuppressWarnings("deprecation") public Future returnSomething(int i) { assertThat(Thread.currentThread().getName()).isNotEqualTo(originalThreadName); return new AsyncResult<>(Integer.toString(i)); @@ -647,6 +617,7 @@ public static class DynamicAsyncMethodsInterfaceBean implements FactoryBean()); DefaultIntroductionAdvisor advisor = new DefaultIntroductionAdvisor((MethodInterceptor) invocation -> { diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java deleted file mode 100644 index b4ac333da9cd..000000000000 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/AsyncResultTests.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.scheduling.annotation; - -import java.io.IOException; -import java.util.HashSet; -import java.util.Set; -import java.util.concurrent.ExecutionException; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -/** - * @author Juergen Hoeller - */ -class AsyncResultTests { - - @Test - @SuppressWarnings({ "deprecation", "removal" }) - public void asyncResultWithCallbackAndValue() throws Exception { - String value = "val"; - final Set values = new HashSet<>(1); - org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forValue(value); - future.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - values.add(result); - } - @Override - public void onFailure(Throwable ex) { - throw new AssertionError("Failure callback not expected: " + ex, ex); - } - }); - assertThat(values).singleElement().isSameAs(value); - assertThat(future.get()).isSameAs(value); - assertThat(future.completable().get()).isSameAs(value); - future.completable().thenAccept(v -> assertThat(v).isSameAs(value)); - } - - @Test - @SuppressWarnings({ "deprecation", "removal" }) - public void asyncResultWithCallbackAndException() { - IOException ex = new IOException(); - final Set values = new HashSet<>(1); - org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forExecutionException(ex); - future.addCallback(new org.springframework.util.concurrent.ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - throw new AssertionError("Success callback not expected: " + result); - } - @Override - public void onFailure(Throwable ex) { - values.add(ex); - } - }); - assertThat(values).singleElement().isSameAs(ex); - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(future::get) - .withCause(ex); - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(future.completable()::get) - .withCause(ex); - } - - @Test - @SuppressWarnings({ "deprecation", "removal" }) - public void asyncResultWithSeparateCallbacksAndValue() throws Exception { - String value = "val"; - final Set values = new HashSet<>(1); - org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forValue(value); - future.addCallback(values::add, ex -> new AssertionError("Failure callback not expected: " + ex)); - assertThat(values).singleElement().isSameAs(value); - assertThat(future.get()).isSameAs(value); - assertThat(future.completable().get()).isSameAs(value); - future.completable().thenAccept(v -> assertThat(v).isSameAs(value)); - } - - @Test - @SuppressWarnings({ "deprecation", "removal" }) - public void asyncResultWithSeparateCallbacksAndException() { - IOException ex = new IOException(); - final Set values = new HashSet<>(1); - org.springframework.util.concurrent.ListenableFuture future = AsyncResult.forExecutionException(ex); - future.addCallback(result -> new AssertionError("Success callback not expected: " + result), values::add); - assertThat(values).singleElement().isSameAs(ex); - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(future::get) - .withCause(ex); - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(future.completable()::get) - .withCause(ex); - } - -} diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java index 17c3f20883ec..ebe8931ae01e 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/EnableAsyncTests.java @@ -29,6 +29,7 @@ import java.util.concurrent.TimeoutException; import org.awaitility.Awaitility; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.Advisor; @@ -49,7 +50,6 @@ import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Lazy; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.scheduling.concurrent.CustomizableThreadFactory; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.stereotype.Component; @@ -630,9 +630,8 @@ public AsyncUncaughtExceptionHandler exceptionHandler() { public static class ExecutorPostProcessor implements BeanPostProcessor { - @Nullable @Override - public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { + public @Nullable Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof ThreadPoolTaskExecutor) { ((ThreadPoolTaskExecutor) bean).setThreadNamePrefix("Post-"); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java index 4c4305600503..87657802e6ef 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/annotation/ScheduledAnnotationBeanPostProcessorTests.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 java.util.concurrent.TimeUnit; import org.assertj.core.api.AbstractAssert; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.AutoClose; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ParameterContext; import org.junit.jupiter.params.ParameterizedTest; @@ -84,14 +84,10 @@ */ class ScheduledAnnotationBeanPostProcessorTests { + @AutoClose private final StaticApplicationContext context = new StaticApplicationContext(); - @AfterEach - void closeContextAfterTest() { - context.close(); - } - @ParameterizedTest @CsvSource(textBlock = """ FixedDelay, 5_000 diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java index 897a0cf87771..e1d24884835b 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/AbstractSchedulingTaskExecutorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2025 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,13 +29,14 @@ import java.util.concurrent.atomic.AtomicReference; import org.awaitility.Awaitility; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; import org.springframework.beans.factory.DisposableBean; -import org.springframework.lang.Nullable; +import org.springframework.core.task.AsyncTaskExecutor; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -47,8 +48,7 @@ */ abstract class AbstractSchedulingTaskExecutorTests { - @SuppressWarnings("removal") - private org.springframework.core.task.AsyncListenableTaskExecutor executor; + private AsyncTaskExecutor executor; protected String testName; @@ -64,8 +64,7 @@ void setup(TestInfo testInfo) { this.executor = buildExecutor(); } - @SuppressWarnings("removal") - protected abstract org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor(); + protected abstract AsyncTaskExecutor buildExecutor(); @AfterEach void shutdownExecutor() throws Exception { @@ -124,22 +123,6 @@ void submitRunnableWithGetAfterShutdown() throws Exception { }); } - @Test - @SuppressWarnings({ "removal", "deprecation" }) - void submitListenableRunnable() { - TestTask task = new TestTask(this.testName, 1); - // Act - org.springframework.util.concurrent.ListenableFuture future = executor.submitListenable(task); - future.addCallback(result -> outcome = result, ex -> outcome = ex); - // Assert - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(future::isDone); - assertThat(outcome).isNull(); - assertThreadNamePrefix(task); - } - @Test void submitCompletableRunnable() { TestTask task = new TestTask(this.testName, 1); @@ -155,21 +138,6 @@ void submitCompletableRunnable() { assertThreadNamePrefix(task); } - @Test - @SuppressWarnings({ "removal", "deprecation" }) - void submitFailingListenableRunnable() { - TestTask task = new TestTask(this.testName, 0); - org.springframework.util.concurrent.ListenableFuture future = executor.submitListenable(task); - future.addCallback(result -> outcome = result, ex -> outcome = ex); - - Awaitility.await() - .dontCatchUncaughtExceptions() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> future.isDone() && outcome != null); - assertThat(outcome.getClass()).isSameAs(RuntimeException.class); - } - @Test void submitFailingCompletableRunnable() { TestTask task = new TestTask(this.testName, 0); @@ -184,26 +152,6 @@ void submitFailingCompletableRunnable() { assertThat(outcome.getClass()).isSameAs(CompletionException.class); } - @Test - @SuppressWarnings({ "removal", "deprecation" }) - void submitListenableRunnableWithGetAfterShutdown() throws Exception { - org.springframework.util.concurrent.ListenableFuture future1 = executor.submitListenable(new TestTask(this.testName, -1)); - org.springframework.util.concurrent.ListenableFuture future2 = executor.submitListenable(new TestTask(this.testName, -1)); - shutdownExecutor(); - - try { - future1.get(1000, TimeUnit.MILLISECONDS); - } - catch (Exception ex) { - // ignore - } - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .untilAsserted(() -> assertThatExceptionOfType(CancellationException.class) - .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); - } - @Test void submitCompletableRunnableWithGetAfterShutdown() throws Exception { CompletableFuture future1 = executor.submitCompletable(new TestTask(this.testName, -1)); @@ -237,57 +185,6 @@ void submitCallableWithGetAfterShutdown() throws Exception { Future future1 = executor.submit(new TestCallable(this.testName, -1)); Future future2 = executor.submit(new TestCallable(this.testName, -1)); shutdownExecutor(); - - try { - future1.get(1000, TimeUnit.MILLISECONDS); - } - catch (Exception ex) { - // ignore - } - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .untilAsserted(() -> assertThatExceptionOfType(CancellationException.class) - .isThrownBy(() -> future2.get(1000, TimeUnit.MILLISECONDS))); - } - - @Test - @SuppressWarnings({ "removal", "deprecation" }) - void submitListenableCallable() { - TestCallable task = new TestCallable(this.testName, 1); - // Act - org.springframework.util.concurrent.ListenableFuture future = executor.submitListenable(task); - future.addCallback(result -> outcome = result, ex -> outcome = ex); - // Assert - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> future.isDone() && outcome != null); - assertThat(outcome.toString().substring(0, this.threadNamePrefix.length())).isEqualTo(this.threadNamePrefix); - } - - @Test - @SuppressWarnings({ "removal", "deprecation" }) - void submitFailingListenableCallable() { - TestCallable task = new TestCallable(this.testName, 0); - // Act - org.springframework.util.concurrent.ListenableFuture future = executor.submitListenable(task); - future.addCallback(result -> outcome = result, ex -> outcome = ex); - // Assert - Awaitility.await() - .dontCatchUncaughtExceptions() - .atMost(5, TimeUnit.SECONDS) - .pollInterval(10, TimeUnit.MILLISECONDS) - .until(() -> future.isDone() && outcome != null); - assertThat(outcome.getClass()).isSameAs(RuntimeException.class); - } - - @Test - @SuppressWarnings({ "removal", "deprecation" }) - void submitListenableCallableWithGetAfterShutdown() throws Exception { - org.springframework.util.concurrent.ListenableFuture future1 = executor.submitListenable(new TestCallable(this.testName, -1)); - org.springframework.util.concurrent.ListenableFuture future2 = executor.submitListenable(new TestCallable(this.testName, -1)); - shutdownExecutor(); assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> { future1.get(1000, TimeUnit.MILLISECONDS); future2.get(1000, TimeUnit.MILLISECONDS); diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java index b1357b121780..ab37b9ca949b 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskExecutorTests.java @@ -25,6 +25,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.NoOpRunnable; import org.springframework.core.task.TaskDecorator; import org.springframework.util.Assert; @@ -42,8 +43,7 @@ class ConcurrentTaskExecutorTests extends AbstractSchedulingTaskExecutorTests { @Override - @SuppressWarnings("removal") - protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + protected AsyncTaskExecutor buildExecutor() { concurrentExecutor.setThreadFactory(new CustomizableThreadFactory(this.threadNamePrefix)); return new ConcurrentTaskExecutor(concurrentExecutor); } diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java index 11711fbc7192..e2380969f63c 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ConcurrentTaskSchedulerTests.java @@ -31,6 +31,7 @@ import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.util.ErrorHandler; @@ -52,8 +53,7 @@ class ConcurrentTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { @Override - @SuppressWarnings("removal") - protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + protected AsyncTaskExecutor buildExecutor() { threadFactory.setThreadNamePrefix(this.threadNamePrefix); scheduler.setTaskDecorator(runnable -> () -> { taskRun.set(true); @@ -79,26 +79,12 @@ void submitRunnableWithGetAfterShutdown() { // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) } - @Test - @SuppressWarnings("deprecation") - @Override - void submitListenableRunnableWithGetAfterShutdown() { - // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) - } - @Test @Override void submitCallableWithGetAfterShutdown() { // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) } - @Test - @SuppressWarnings("deprecation") - @Override - void submitListenableCallableWithGetAfterShutdown() { - // decorated Future cannot be cancelled on shutdown with ConcurrentTaskScheduler (see above) - } - @Test void executeFailingRunnableWithErrorHandler() { diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java index d69ac85a642f..abd600226abe 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/DecoratedThreadPoolTaskExecutorTests.java @@ -16,6 +16,7 @@ package org.springframework.scheduling.concurrent; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.support.DelegatingErrorHandlingRunnable; import org.springframework.scheduling.support.TaskUtils; @@ -26,8 +27,7 @@ class DecoratedThreadPoolTaskExecutorTests extends AbstractSchedulingTaskExecutorTests { @Override - @SuppressWarnings("removal") - protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + protected AsyncTaskExecutor buildExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); executor.setTaskDecorator(runnable -> new DelegatingErrorHandlingRunnable(runnable, TaskUtils.LOG_AND_PROPAGATE_ERROR_HANDLER)); diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java index b3cd12d52e3e..7bcab7833585 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/SimpleAsyncTaskSchedulerTests.java @@ -27,6 +27,7 @@ import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.util.ErrorHandler; @@ -45,8 +46,7 @@ class SimpleAsyncTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests @Override - @SuppressWarnings("removal") - protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + protected AsyncTaskExecutor buildExecutor() { scheduler.setTaskDecorator(runnable -> () -> { taskRun.set(true); runnable.run(); @@ -62,12 +62,6 @@ void submitRunnableWithGetAfterShutdown() { // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler } - @Test - @Override - void submitListenableRunnableWithGetAfterShutdown() { - // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler - } - @Test @Override void submitCompletableRunnableWithGetAfterShutdown() { @@ -80,13 +74,6 @@ void submitCallableWithGetAfterShutdown() { // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler } - @Test - @SuppressWarnings("deprecation") - @Override - void submitListenableCallableWithGetAfterShutdown() { - // decorated Future cannot be cancelled on shutdown with SimpleAsyncTaskScheduler - } - @Test @Override void submitCompletableCallableWithGetAfterShutdown() { diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java index 5201af2a34ca..35a62f44b331 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskExecutorTests.java @@ -23,6 +23,8 @@ import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; @@ -41,8 +43,7 @@ class ThreadPoolTaskExecutorTests extends AbstractSchedulingTaskExecutorTests { @Override - @SuppressWarnings("removal") - protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + protected AsyncTaskExecutor buildExecutor() { executor.setThreadNamePrefix(this.threadNamePrefix); executor.setMaxPoolSize(1); executor.afterPropertiesSet(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java index 63cbeafbcb22..712d69ba5a6b 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/concurrent/ThreadPoolTaskSchedulerTests.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.RepeatedTest; import org.junit.jupiter.api.Test; +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.TriggerContext; import org.springframework.util.ErrorHandler; @@ -49,8 +50,7 @@ class ThreadPoolTaskSchedulerTests extends AbstractSchedulingTaskExecutorTests { @Override - @SuppressWarnings("removal") - protected org.springframework.core.task.AsyncListenableTaskExecutor buildExecutor() { + protected AsyncTaskExecutor buildExecutor() { scheduler.setTaskDecorator(runnable -> () -> { taskRun.set(true); runnable.run(); diff --git a/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java b/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java index ad1f6222d367..c51cf92192b1 100644 --- a/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java +++ b/spring-context/src/test/java/org/springframework/scheduling/support/PeriodicTriggerTests.java @@ -20,9 +20,9 @@ import java.time.Instant; import java.time.temporal.ChronoUnit; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.scheduling.TriggerContext; import org.springframework.util.NumberUtils; @@ -227,8 +227,7 @@ private static TriggerContext context(@Nullable Object scheduled, @Nullable Obje return new TestTriggerContext(toInstant(scheduled), toInstant(actual), toInstant(completion)); } - @Nullable - private static Instant toInstant(@Nullable Object o) { + private static @Nullable Instant toInstant(@Nullable Object o) { if (o == null) { return null; } @@ -249,14 +248,11 @@ private static Instant toInstant(@Nullable Object o) { private static class TestTriggerContext implements TriggerContext { - @Nullable - private final Instant scheduled; + private final @Nullable Instant scheduled; - @Nullable - private final Instant actual; + private final @Nullable Instant actual; - @Nullable - private final Instant completion; + private final @Nullable Instant completion; TestTriggerContext(@Nullable Instant scheduled, @Nullable Instant actual, @Nullable Instant completion) { diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java index 812f4c442338..18f58f6ba119 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/GroovyScriptFactoryTests.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. @@ -277,9 +277,9 @@ void testNonStaticPrototypeScript() { @Test void testScriptCompilationException() { - assertThatExceptionOfType(NestedRuntimeException.class).isThrownBy(() -> - new ClassPathXmlApplicationContext("org/springframework/scripting/groovy/groovyBrokenContext.xml")) - .matches(ex -> ex.contains(ScriptCompilationException.class)); + assertThatExceptionOfType(NestedRuntimeException.class) + .isThrownBy(() -> new ClassPathXmlApplicationContext("org/springframework/scripting/groovy/groovyBrokenContext.xml")) + .matches(ex -> ex.contains(ScriptCompilationException.class)); } @Test @@ -288,11 +288,10 @@ void testScriptedClassThatDoesNotHaveANoArgCtor() throws Exception { String badScript = "class Foo { public Foo(String foo) {}}"; given(script.getScriptAsString()).willReturn(badScript); given(script.suggestedClassName()).willReturn("someName"); - GroovyScriptFactory factory = new GroovyScriptFactory(ScriptFactoryPostProcessor.INLINE_SCRIPT_PREFIX - + badScript); - assertThatExceptionOfType(ScriptCompilationException.class).isThrownBy(() -> - factory.getScriptedObject(script)) - .matches(ex -> ex.contains(NoSuchMethodException.class)); + GroovyScriptFactory factory = new GroovyScriptFactory(ScriptFactoryPostProcessor.INLINE_SCRIPT_PREFIX + badScript); + assertThatExceptionOfType(ScriptCompilationException.class) + .isThrownBy(() -> factory.getScriptedObject(script)) + .matches(ex -> ex.contains(NoSuchMethodException.class)); } @Test @@ -327,27 +326,24 @@ void testWithTwoClassesDefinedInTheOneGroovyFile_WrongClassFirst() { @Test void testCtorWithNullScriptSourceLocator() { - assertThatIllegalArgumentException().isThrownBy(() -> - new GroovyScriptFactory(null)); + assertThatIllegalArgumentException().isThrownBy(() -> new GroovyScriptFactory(null)); } @Test void testCtorWithEmptyScriptSourceLocator() { - assertThatIllegalArgumentException().isThrownBy(() -> - new GroovyScriptFactory("")); + assertThatIllegalArgumentException().isThrownBy(() -> new GroovyScriptFactory("")); } @Test void testCtorWithWhitespacedScriptSourceLocator() { - assertThatIllegalArgumentException().isThrownBy(() -> - new GroovyScriptFactory("\n ")); + assertThatIllegalArgumentException().isThrownBy(() -> new GroovyScriptFactory("\n ")); } @Test void testWithInlineScriptWithLeadingWhitespace() { - assertThatExceptionOfType(BeanCreationException.class).as("'inline:' prefix was preceded by whitespace").isThrownBy(() -> - new ClassPathXmlApplicationContext("lwspBadGroovyContext.xml", getClass())) - .matches(ex -> ex.contains(FileNotFoundException.class)); + assertThatExceptionOfType(BeanCreationException.class).as("'inline:' prefix was preceded by whitespace") + .isThrownBy(() -> new ClassPathXmlApplicationContext("lwspBadGroovyContext.xml", getClass())) + .matches(ex -> ex.contains(FileNotFoundException.class)); } @Test @@ -364,8 +360,8 @@ void testGetScriptedObjectDoesNotChokeOnNullInterfacesBeingPassedIn() throws Exc @Test void testGetScriptedObjectDoesChokeOnNullScriptSourceBeingPassedIn() { GroovyScriptFactory factory = new GroovyScriptFactory("a script source locator (doesn't matter here)"); - assertThatNullPointerException().as("NullPointerException as per contract ('null' ScriptSource supplied)").isThrownBy(() -> - factory.getScriptedObject(null)); + assertThatNullPointerException().as("NullPointerException as per contract ('null' ScriptSource supplied)") + .isThrownBy(() -> factory.getScriptedObject(null)); } @Test diff --git a/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java b/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java index 8bcc9ffde890..4f219bb84f69 100644 --- a/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java +++ b/spring-context/src/test/java/org/springframework/scripting/groovy/LogUserAdvice.java @@ -18,9 +18,10 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.MethodBeforeAdvice; import org.springframework.aop.ThrowsAdvice; -import org.springframework.lang.Nullable; public class LogUserAdvice implements MethodBeforeAdvice, ThrowsAdvice { diff --git a/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java b/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java index 5c36f02ce4f2..4b2f9ceb48f1 100644 --- a/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java +++ b/spring-context/src/test/java/org/springframework/ui/ModelMapTests.java @@ -25,11 +25,11 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.framework.ProxyFactory; import org.springframework.beans.testfixture.beans.TestBean; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java index 4c81ca65cc3c..c61792783dba 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderConstructTests.java @@ -23,11 +23,11 @@ import java.util.Set; import jakarta.validation.constraints.NotNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.ResolvableType; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import static org.assertj.core.api.Assertions.assertThat; @@ -302,8 +302,7 @@ static class NestedDataClass { private final String param1; - @Nullable - private final DataClass nestedParam2; + private final @Nullable DataClass nestedParam2; public NestedDataClass(String param1, @Nullable DataClass nestedParam2) { this.param1 = param1; @@ -314,8 +313,7 @@ public String param1() { return this.param1; } - @Nullable - public DataClass nestedParam2() { + public @Nullable DataClass nestedParam2() { return this.nestedParam2; } } diff --git a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java index 81ee18a8a438..77549a817716 100644 --- a/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java +++ b/spring-context/src/test/java/org/springframework/validation/DataBinderTests.java @@ -36,6 +36,7 @@ import java.util.Set; import java.util.TreeSet; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.BeanWrapper; @@ -62,7 +63,6 @@ import org.springframework.format.number.NumberStyleFormatter; import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.tests.sample.beans.BeanWithObjectProperty; import org.springframework.util.StringUtils; diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java index 27be1d5fbe2f..fd6a03bd6a62 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/BeanValidationBeanRegistrationAotProcessorTests.java @@ -32,6 +32,7 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.Pattern; import org.hibernate.validator.internal.constraintvalidators.bv.PatternValidator; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -45,7 +46,6 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.OverridingClassLoader; -import org.springframework.lang.Nullable; import static java.lang.annotation.ElementType.ANNOTATION_TYPE; import static java.lang.annotation.ElementType.CONSTRUCTOR; @@ -79,7 +79,7 @@ void shouldProcessMethodParameterLevelConstraint() { process(MethodParameterLevelConstraint.class); assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2); assertThat(RuntimeHintsPredicates.reflection().onType(MethodParameterLevelConstraint.class) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints()); } @@ -89,7 +89,7 @@ void shouldProcessConstructorParameterLevelConstraint() { process(ConstructorParameterLevelConstraint.class); assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2); assertThat(RuntimeHintsPredicates.reflection().onType(ConstructorParameterLevelConstraint.class) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints()); } @@ -99,7 +99,7 @@ void shouldProcessPropertyLevelConstraint() { process(PropertyLevelConstraint.class); assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2); assertThat(RuntimeHintsPredicates.reflection().onType(PropertyLevelConstraint.class) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); assertThat(RuntimeHintsPredicates.reflection().onType(ExistsValidator.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints()); } @@ -109,7 +109,7 @@ void shouldProcessGenericTypeLevelConstraint() { process(GenericTypeLevelConstraint.class); assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(2); assertThat(RuntimeHintsPredicates.reflection().onType(GenericTypeLevelConstraint.class) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); assertThat(RuntimeHintsPredicates.reflection().onType(PatternValidator.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints()); } @@ -119,9 +119,9 @@ void shouldProcessTransitiveGenericTypeLevelConstraint() { process(TransitiveGenericTypeLevelConstraint.class); assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(3); assertThat(RuntimeHintsPredicates.reflection().onType(TransitiveGenericTypeLevelConstraint.class) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); assertThat(RuntimeHintsPredicates.reflection().onType(Exclude.class) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); assertThat(RuntimeHintsPredicates.reflection().onType(PatternValidator.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(this.generationContext.getRuntimeHints()); } @@ -132,7 +132,7 @@ void shouldProcessRecursiveGenericsWithoutInfiniteRecursion(Class beanClass) process(beanClass); assertThat(this.generationContext.getRuntimeHints().reflection().typeHints()).hasSize(1); assertThat(RuntimeHintsPredicates.reflection().onType(beanClass) - .withMemberCategory(MemberCategory.DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); + .withMemberCategory(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(this.generationContext.getRuntimeHints()); } @Test // gh-33940 @@ -150,8 +150,7 @@ private void process(Class beanClass) { } } - @Nullable - private BeanRegistrationAotContribution createContribution(Class beanClass) { + private @Nullable BeanRegistrationAotContribution createContribution(Class beanClass) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); return this.processor.processAheadOfTime(RegisteredBean.of(beanFactory, beanClass.getName())); diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java index 76a3caae13ca..b5437b7e9c40 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterPropertyPathTests.java @@ -25,10 +25,10 @@ import jakarta.validation.Valid; import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.Size; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.validation.FieldError; import org.springframework.validation.method.MethodValidationResult; diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java index 8326a5c23874..6eac0de1ac01 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationAdapterTests.java @@ -37,12 +37,12 @@ import jakarta.validation.constraints.Size; import jakarta.validation.constraintvalidation.SupportedValidationTarget; import jakarta.validation.constraintvalidation.ValidationTarget; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.context.MessageSourceResolvable; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.validation.FieldError; import org.springframework.validation.method.MethodValidationResult; diff --git a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java index a058f2c24112..cc16e59096d8 100644 --- a/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java +++ b/spring-context/src/test/java/org/springframework/validation/beanvalidation/MethodValidationProxyTests.java @@ -28,6 +28,7 @@ import jakarta.validation.groups.Default; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @@ -42,7 +43,6 @@ import org.springframework.context.annotation.Lazy; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.BridgeMethodResolver; -import org.springframework.lang.Nullable; import org.springframework.scheduling.annotation.Async; import org.springframework.scheduling.annotation.AsyncAnnotationAdvisor; import org.springframework.scheduling.annotation.AsyncAnnotationBeanPostProcessor; @@ -223,9 +223,8 @@ static class MyValidClientInterfaceMethodInterceptor implements MethodIntercepto private final MyValidBean myValidBean = new MyValidBean(); - @Nullable @Override - public Object invoke(MethodInvocation invocation) { + public @Nullable Object invoke(MethodInvocation invocation) { Method method; try { method = ClassUtils.getMethod(MyValidBean.class, invocation.getMethod().getName(), (Class[]) null); 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 new file mode 100644 index 000000000000..41adc23a5d87 --- /dev/null +++ b/spring-context/src/test/kotlin/org/springframework/context/annotation/BeanRegistrarDslConfigurationTests.kt @@ -0,0 +1,133 @@ +/* + * Copyright 2002-2025 the original author 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.context.annotation + +import org.assertj.core.api.Assertions.assertThat +import org.assertj.core.api.Assertions.assertThatThrownBy +import org.junit.jupiter.api.Test +import org.springframework.beans.factory.BeanRegistrarDsl +import org.springframework.beans.factory.InitializingBean +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 + +/** + * Kotlin tests leveraging [BeanRegistrarDsl]. + * + * @author Sebastien Deleuze + */ +class BeanRegistrarDslConfigurationTests { + + @Test + fun beanRegistrar() { + 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 { 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.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!") + assertThat(context.getBean().initialized).isTrue() + } + + @Test + fun genericBeanRegistrar() { + val context = AnnotationConfigApplicationContext(GenericBeanRegistrarKotlinConfiguration::class.java) + val beanDefinition = context.getBeanDefinition("fooSupplier") as RootBeanDefinition + assertThat(beanDefinition.resolvableType.resolveGeneric(0)).isEqualTo(Foo::class.java) + } + + @Test + fun chainedBeanRegistrar() { + val context = AnnotationConfigApplicationContext(ChainedBeanRegistrarKotlinConfiguration::class.java) + assertThat(context.getBean().foo).isEqualTo(context.getBean()) + } + + 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 + + override fun afterPropertiesSet() { + initialized = true + } + + } + + @Configuration + @Import(SampleBeanRegistrar::class) + internal class BeanRegistrarKotlinConfiguration + + private class SampleBeanRegistrar : BeanRegistrarDsl({ + registerBean("foo") + registerAlias("foo", "fooAlias") + registerBean( + name = "bar", + prototype = true, + lazyInit = true, + description = "Custom description") { + Bar(bean()) + } + profile("baz") { + registerBean { Baz(env.getRequiredProperty("hello.world")) } + } + registerBean() + registerBean(::booFactory, "fooFactory") + }) + + @Configuration + @Import(GenericBeanRegistrar::class) + internal class GenericBeanRegistrarKotlinConfiguration + + private class GenericBeanRegistrar : BeanRegistrarDsl({ + registerBean>(name = "fooSupplier") { + Supplier { Foo() } + } + }) + + @Configuration + @Import(ChainedBeanRegistrar::class) + internal class ChainedBeanRegistrarKotlinConfiguration + + private class ChainedBeanRegistrar : BeanRegistrarDsl({ + register(SampleBeanRegistrar()) + }) +} + +fun booFactory() = BeanRegistrarDslConfigurationTests.Boo("booFactory") diff --git a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt index 4252607b5868..1895bbb75724 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/aot/KotlinReflectionBeanRegistrationAotProcessorTests.kt @@ -48,16 +48,11 @@ class KotlinReflectionBeanRegistrationAotProcessorTests { @Test fun shouldProcessKotlinBean() { process(SampleKotlinBean::class.java) + assertThat(RuntimeHintsPredicates.reflection().onType(SampleKotlinBean::class.java)) + .accepts(generationContext.runtimeHints) assertThat( - RuntimeHintsPredicates.reflection() - .onType(SampleKotlinBean::class.java) - .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS) - ).accepts(generationContext.runtimeHints) - assertThat( - RuntimeHintsPredicates.reflection() - .onType(BaseKotlinBean::class.java) - .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS) - ).accepts(generationContext.runtimeHints) + RuntimeHintsPredicates.reflection().onType(BaseKotlinBean::class.java)) + .accepts(generationContext.runtimeHints) } @Test @@ -72,7 +67,6 @@ class KotlinReflectionBeanRegistrationAotProcessorTests { assertThat( RuntimeHintsPredicates.reflection() .onType(OuterBean.NestedBean::class.java) - .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS) .and(RuntimeHintsPredicates.reflection().onType(OuterBean::class.java)) ).accepts(generationContext.runtimeHints) } diff --git a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt index 11bb65c8a91c..f4770b5f6c28 100644 --- a/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.kt +++ b/spring-context/src/test/kotlin/org/springframework/context/support/BeanDefinitionDslTests.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. @@ -14,6 +14,7 @@ * limitations under the License. */ +@file:Suppress("DEPRECATION") package org.springframework.context.support import org.assertj.core.api.Assertions.assertThat diff --git a/spring-context/src/test/resources/example/scannable/spring.components b/spring-context/src/test/resources/example/scannable/spring.components index 3fdf592b1965..8c298cd44d96 100644 --- a/spring-context/src/test/resources/example/scannable/spring.components +++ b/spring-context/src/test/resources/example/scannable/spring.components @@ -10,9 +10,4 @@ example.scannable.ServiceInvocationCounter=org.springframework.stereotype.Compon example.scannable.sub.BarComponent=org.springframework.stereotype.Component example.scannable.JakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.scannable.JakartaNamedComponent=jakarta.inject.Named -example.scannable.JavaxManagedBeanComponent=javax.annotation.ManagedBean -example.scannable.JavaxNamedComponent=javax.inject.Named -example.indexed.IndexedJakartaManagedBeanComponent=jakarta.annotation.ManagedBean example.indexed.IndexedJakartaNamedComponent=jakarta.inject.Named -example.indexed.IndexedJavaxManagedBeanComponent=javax.annotation.ManagedBean -example.indexed.IndexedJavaxNamedComponent=javax.inject.Named 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-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/GenericBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/GenericBeanRegistrar.java new file mode 100644 index 000000000000..c2d61d2b48f3 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/GenericBeanRegistrar.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.context.testfixture.beans.factory; + +import java.util.function.Supplier; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.core.env.Environment; + +public class GenericBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + ParameterizedTypeReference> type = new ParameterizedTypeReference<>() {}; + registry.registerBean("fooSupplier", Supplier.class, spec -> spec.targetType(type) + .supplier(context-> (Supplier) Foo::new)); + } + + public record Foo() {} +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/ImportAwareBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/ImportAwareBeanRegistrar.java new file mode 100644 index 000000000000..8915117cf9de --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/ImportAwareBeanRegistrar.java @@ -0,0 +1,48 @@ +/* + * Copyright 2002-2025 the original author 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.context.testfixture.beans.factory; + +import org.jspecify.annotations.Nullable; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.context.annotation.ImportAware; +import org.springframework.core.env.Environment; +import org.springframework.core.type.AnnotationMetadata; + +public class ImportAwareBeanRegistrar implements BeanRegistrar, ImportAware { + + @Nullable + private AnnotationMetadata importMetadata; + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean(ClassNameHolder.class, spec -> spec.supplier(context -> + new ClassNameHolder(this.importMetadata == null ? null : this.importMetadata.getClassName()))); + } + + @Override + public void setImportMetadata(AnnotationMetadata importMetadata) { + this.importMetadata = importMetadata; + } + + public @Nullable AnnotationMetadata getImportMetadata() { + return this.importMetadata; + } + + public record ClassNameHolder(@Nullable String className) {} +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/SampleBeanRegistrar.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/SampleBeanRegistrar.java new file mode 100644 index 000000000000..e29db751321a --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/beans/factory/SampleBeanRegistrar.java @@ -0,0 +1,56 @@ +/* + * Copyright 2002-2025 the original author 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.context.testfixture.beans.factory; + +import jakarta.annotation.PostConstruct; + +import org.springframework.beans.factory.BeanRegistrar; +import org.springframework.beans.factory.BeanRegistry; +import org.springframework.core.env.Environment; + +public class SampleBeanRegistrar implements BeanRegistrar { + + @Override + public void register(BeanRegistry registry, Environment env) { + registry.registerBean("foo", Foo.class); + registry.registerAlias("foo", "fooAlias"); + registry.registerBean("bar", Bar.class, spec -> spec + .prototype() + .lazyInit() + .description("Custom description") + .supplier(context -> new Bar(context.bean(Foo.class)))); + if (env.matchesProfiles("baz")) { + registry.registerBean(Baz.class, spec -> spec + .supplier(context -> new Baz("Hello World!"))); + } + registry.registerBean(Init.class); + } + + public record Foo() {} + public record Bar(Foo foo) {} + public record Baz(String message) {} + + public static class Init { + + public boolean initialized = false; + + @PostConstruct + public void postConstruct() { + initialized = true; + } + } +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java index 4fd7e9c0456d..899662bdc6d7 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/cache/beans/TestEntity.java @@ -18,7 +18,8 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/BeanRegistrarConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/BeanRegistrarConfiguration.java new file mode 100644 index 000000000000..1360caec7e1c --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/BeanRegistrarConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2025 the original author 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.context.testfixture.context.annotation.registrar; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.testfixture.beans.factory.SampleBeanRegistrar; + +@Configuration +@Import(SampleBeanRegistrar.class) +public class BeanRegistrarConfiguration { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/GenericBeanRegistrarConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/GenericBeanRegistrarConfiguration.java new file mode 100644 index 000000000000..69e211686088 --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/GenericBeanRegistrarConfiguration.java @@ -0,0 +1,26 @@ +/* + * Copyright 2002-2025 the original author 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.context.testfixture.context.annotation.registrar; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.testfixture.beans.factory.GenericBeanRegistrar; + +@Configuration +@Import(GenericBeanRegistrar.class) +public class GenericBeanRegistrarConfiguration { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ImportAwareBeanRegistrarConfiguration.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ImportAwareBeanRegistrarConfiguration.java new file mode 100644 index 000000000000..7b4b21b4615b --- /dev/null +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/context/annotation/registrar/ImportAwareBeanRegistrarConfiguration.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.context.testfixture.context.annotation.registrar; + +import org.springframework.context.annotation.Import; +import org.springframework.context.testfixture.beans.factory.ImportAwareBeanRegistrar; + +@Import(ImportAwareBeanRegistrar.class) +public class ImportAwareBeanRegistrarConfiguration { +} diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java index d86dafb55329..36d7f40a4758 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/index/CandidateComponentsTestClassLoader.java @@ -22,8 +22,9 @@ import java.util.Enumeration; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * A test {@link ClassLoader} that can be used in a testing context to control the @@ -66,11 +67,9 @@ public static ClassLoader index(ClassLoader classLoader, Resource... resources) } - @Nullable - private final Enumeration resourceUrls; + private final @Nullable Enumeration resourceUrls; - @Nullable - private final IOException cause; + private final @Nullable IOException cause; public CandidateComponentsTestClassLoader(ClassLoader classLoader, Enumeration resourceUrls) { super(classLoader); diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java index 92b0e3fc4b24..cd1d0dee9e23 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContext.java @@ -33,8 +33,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -213,8 +213,7 @@ public Hashtable getEnvironment() { } @Override - @Nullable - public Object addToEnvironment(String propName, Object propVal) { + public @Nullable Object addToEnvironment(String propName, Object propVal) { return this.environment.put(propName, propVal); } diff --git a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java index fa253ada9863..19ca64a6672a 100644 --- a/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java +++ b/spring-context/src/testFixtures/java/org/springframework/context/testfixture/jndi/SimpleNamingContextBuilder.java @@ -26,8 +26,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -88,8 +88,7 @@ public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder { /** An instance of this class bound to JNDI. */ - @Nullable - private static volatile SimpleNamingContextBuilder activated; + private static volatile @Nullable SimpleNamingContextBuilder activated; private static boolean initialized = false; @@ -101,8 +100,7 @@ public class SimpleNamingContextBuilder implements InitialContextFactoryBuilder * @return the current SimpleNamingContextBuilder instance, * or {@code null} if none */ - @Nullable - public static SimpleNamingContextBuilder getCurrentContextBuilder() { + public static @Nullable SimpleNamingContextBuilder getCurrentContextBuilder() { return activated; } diff --git a/spring-core-test/spring-core-test.gradle b/spring-core-test/spring-core-test.gradle index 55f2a8b8f4fb..a398ed30c3bf 100644 --- a/spring-core-test/spring-core-test.gradle +++ b/spring-core-test/spring-core-test.gradle @@ -2,11 +2,10 @@ description = "Spring Core Test" dependencies { api(project(":spring-core")) - api("org.assertj:assertj-core") - api("org.junit.jupiter:junit-jupiter-api") - compileOnly("org.junit.jupiter:junit-jupiter") - compileOnly("org.junit.platform:junit-platform-engine") - compileOnly("org.junit.platform:junit-platform-launcher") + optional("org.assertj:assertj-core") + optional("org.junit.jupiter:junit-jupiter-api") + compileOnly("org.junit.jupiter:junit-jupiter-params") // Used in CompileWithForkedClassLoaderExtension Javadoc + compileOnly("org.junit.platform:junit-platform-launcher") // Used in CompileWithForkedClassLoaderExtension implementation("com.thoughtworks.qdox:qdox") } 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 35610a92220f..d734e817e7a4 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 @@ -31,7 +31,7 @@ import java.util.ResourceBundle; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Instrumented version of JDK methods to be used by bytecode rewritten by the {@link RuntimeHintsAgent}. @@ -44,7 +44,7 @@ * @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() { @@ -232,8 +232,7 @@ public static Field classgetField(Class clazz, String name) throws NoSuchFiel return result; } - @Nullable - public static URL classgetResource(Class clazz, String name) { + public static @Nullable URL classgetResource(Class clazz, String name) { URL result = clazz.getResource(name); RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) .onInstance(clazz).withArgument(name).returnValue(result).build(); @@ -241,8 +240,7 @@ public static URL classgetResource(Class clazz, String name) { return result; } - @Nullable - public static InputStream classgetResourceAsStream(Class clazz, String name) { + public static @Nullable InputStream classgetResourceAsStream(Class clazz, String name) { InputStream result = clazz.getResourceAsStream(name); RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCEASSTREAM) .onInstance(clazz).withArgument(name).returnValue(result).build(); @@ -267,8 +265,7 @@ public static Class classloaderloadClass(ClassLoader classLoader, String name return result; } - @Nullable - public static URL classloadergetResource(ClassLoader classLoader, String name) { + public static @Nullable URL classloadergetResource(ClassLoader classLoader, String name) { URL result = classLoader.getResource(name); RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_GETRESOURCE) .onInstance(classLoader).withArgument(name).returnValue(result).build(); @@ -276,8 +273,7 @@ public static URL classloadergetResource(ClassLoader classLoader, String name) { return result; } - @Nullable - public static InputStream classloadergetResourceAsStream(ClassLoader classLoader, String name) { + public static @Nullable InputStream classloadergetResourceAsStream(ClassLoader classLoader, String name) { InputStream result = classLoader.getResourceAsStream(name); RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASSLOADER_GETRESOURCEASSTREAM) .onInstance(classLoader).withArgument(name).returnValue(result).build(); @@ -338,8 +334,8 @@ public static Object methodinvoke(Method method, Object object, Object... argume Object result = null; boolean accessibilityChanged = false; try { - if (!Modifier.isPublic(method.getModifiers()) - || !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { + if (!Modifier.isPublic(method.getModifiers()) || + !Modifier.isPublic(method.getDeclaringClass().getModifiers())) { method.setAccessible(true); accessibilityChanged = true; } diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java index 2c320d67e5e6..96e328dbed29 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InstrumentedMethod.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. @@ -20,13 +20,11 @@ import java.lang.reflect.Field; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; -import java.lang.reflect.Modifier; import java.lang.reflect.Proxy; import java.util.ResourceBundle; import java.util.function.Function; import java.util.function.Predicate; -import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; @@ -67,8 +65,7 @@ enum InstrumentedMethod { CLASS_GETCLASSES(Class.class, "getClasses", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.DECLARED_CLASSES, MemberCategory.PUBLIC_CLASSES); + return reflection().onType(TypeReference.of(thisClass)); } ), @@ -81,7 +78,7 @@ enum InstrumentedMethod { if (constructor == null) { return runtimeHints -> false; } - return reflection().onConstructor(constructor).introspect(); + return reflection().onType(constructor.getDeclaringClass()); } ), @@ -91,9 +88,7 @@ enum InstrumentedMethod { CLASS_GETCONSTRUCTORS(Class.class, "getConstructors", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)).withAnyMemberCategory( - MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + return reflection().onType(TypeReference.of(thisClass)); } ), @@ -103,7 +98,7 @@ enum InstrumentedMethod { CLASS_GETDECLAREDCLASSES(Class.class, "getDeclaredClasses", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)).withMemberCategory(MemberCategory.DECLARED_CLASSES); + return reflection().onType(TypeReference.of(thisClass)); } ), @@ -116,9 +111,7 @@ enum InstrumentedMethod { if (constructor == null) { return runtimeHints -> false; } - TypeReference thisType = invocation.getInstanceTypeReference(); - return reflection().onType(thisType).withMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS) - .or(reflection().onConstructor(constructor).introspect()); + return reflection().onType(constructor.getDeclaringClass()); } ), @@ -128,8 +121,7 @@ enum InstrumentedMethod { CLASS_GETDECLAREDCONSTRUCTORS(Class.class, "getDeclaredConstructors", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + return reflection().onType(TypeReference.of(thisClass)); }), /** @@ -141,9 +133,7 @@ enum InstrumentedMethod { if (field == null) { return runtimeHints -> false; } - TypeReference thisType = invocation.getInstanceTypeReference(); - return reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS) - .or(reflection().onField(field)); + return reflection().onType(field.getDeclaringClass()); } ), @@ -153,7 +143,7 @@ enum InstrumentedMethod { CLASS_GETDECLAREDFIELDS(Class.class, "getDeclaredFields", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)).withMemberCategory(MemberCategory.DECLARED_FIELDS); + return reflection().onType(TypeReference.of(thisClass)); } ), @@ -166,10 +156,7 @@ enum InstrumentedMethod { if (method == null) { return runtimeHints -> false; } - TypeReference thisType = invocation.getInstanceTypeReference(); - return reflection().onType(thisType) - .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) - .or(reflection().onMethod(method).introspect()); + return reflection().onType(method.getDeclaringClass()); } ), @@ -179,26 +166,20 @@ enum InstrumentedMethod { CLASS_GETDECLAREDMETHODS(Class.class, "getDeclaredMethods", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS); + return reflection().onType(TypeReference.of(thisClass)); } ), /** * {@link Class#getField(String)}. */ - @SuppressWarnings("NullAway") CLASS_GETFIELD(Class.class, "getField", HintType.REFLECTION, invocation -> { Field field = invocation.getReturnValue(); if (field == null) { return runtimeHints -> false; } - TypeReference thisType = invocation.getInstanceTypeReference(); - return reflection().onType(thisType).withMemberCategory(MemberCategory.PUBLIC_FIELDS) - .and(runtimeHints -> Modifier.isPublic(field.getModifiers())) - .or(reflection().onType(thisType).withMemberCategory(MemberCategory.DECLARED_FIELDS)) - .or(reflection().onField(invocation.getReturnValue())); + return reflection().onType(field.getDeclaringClass()); }), /** @@ -207,8 +188,7 @@ enum InstrumentedMethod { CLASS_GETFIELDS(Class.class, "getFields", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)) - .withAnyMemberCategory(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS); + return reflection().onType(TypeReference.of(thisClass)); } ), @@ -221,12 +201,7 @@ enum InstrumentedMethod { if (method == null) { return runtimeHints -> false; } - TypeReference thisType = invocation.getInstanceTypeReference(); - return reflection().onType(thisType).withAnyMemberCategory(MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INVOKE_PUBLIC_METHODS) - .and(runtimeHints -> Modifier.isPublic(method.getModifiers())) - .or(reflection().onType(thisType).withAnyMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS, MemberCategory.INVOKE_DECLARED_METHODS)) - .or(reflection().onMethod(method).introspect()) - .or(reflection().onMethod(method).invoke()); + return reflection().onType(method.getDeclaringClass()); } ), @@ -236,9 +211,7 @@ enum InstrumentedMethod { CLASS_GETMETHODS(Class.class, "getMethods", HintType.REFLECTION, invocation -> { Class thisClass = invocation.getInstance(); - return reflection().onType(TypeReference.of(thisClass)).withAnyMemberCategory( - MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS); + return reflection().onType(TypeReference.of(thisClass)); } ), @@ -258,25 +231,25 @@ enum InstrumentedMethod { * {@link Constructor#newInstance(Object...)}. */ CONSTRUCTOR_NEWINSTANCE(Constructor.class, "newInstance", HintType.REFLECTION, - invocation -> reflection().onConstructor(invocation.getInstance()).invoke()), + invocation -> reflection().onConstructorInvocation(invocation.getInstance())), /** * {@link Method#invoke(Object, Object...)}. */ METHOD_INVOKE(Method.class, "invoke", HintType.REFLECTION, - invocation -> reflection().onMethod(invocation.getInstance()).invoke()), + invocation -> reflection().onMethodInvocation(invocation.getInstance())), /** * {@link Field#get(Object)}. */ FIELD_GET(Field.class, "get", HintType.REFLECTION, - invocation -> reflection().onField(invocation.getInstance())), + invocation -> reflection().onFieldAccess(invocation.getInstance())), /** * {@link Field#set(Object, Object)}. */ FIELD_SET(Field.class, "set", HintType.REFLECTION, - invocation -> reflection().onField(invocation.getInstance())), + invocation -> reflection().onFieldAccess(invocation.getInstance())), /* diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java index f9cc6abd17f3..f3babf532ad6 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassTransformer.java @@ -21,8 +21,9 @@ import java.security.ProtectionDomain; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.ClassReader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java index 4e7daa8f4119..6571a2ab61fd 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/InvocationsRecorderClassVisitor.java @@ -78,8 +78,8 @@ class InvocationsRecorderMethodVisitor extends MethodVisitor implements Opcodes static { for (InstrumentedMethod method : InstrumentedMethod.values()) { MethodReference methodReference = method.methodReference(); - instrumentedMethods.add(methodReference.getClassName().replace('.', '/') - + "#" + methodReference.getMethodName()); + instrumentedMethods.add(methodReference.getClassName().replace('.', '/') + + "#" + methodReference.getMethodName()); } } diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java b/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java index 4a2f119445ec..0ab3fa387e02 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/MethodReference.java @@ -18,7 +18,7 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Reference to a Java method, identified by its owner class and the method name. diff --git a/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java index 7572fcebdde4..20e411e7eeeb 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/RecordedInvocation.java @@ -20,9 +20,10 @@ import java.util.List; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -36,15 +37,13 @@ */ public final class RecordedInvocation { - @Nullable - private final Object instance; + private final @Nullable Object instance; private final InstrumentedMethod instrumentedMethod; private final Object[] arguments; - @Nullable - private final Object returnValue; + private final @Nullable Object returnValue; private final List stackFrames; @@ -160,8 +159,7 @@ public List getArgumentTypes(int index) { * @return the value returned by the invocation */ @SuppressWarnings("unchecked") - @Nullable - public T getReturnValue() { + public @Nullable T getReturnValue() { return (T) this.returnValue; } @@ -192,15 +190,13 @@ public String toString() { */ public static class Builder { - @Nullable - private Object instance; + private @Nullable Object instance; private final InstrumentedMethod instrumentedMethod; private Object[] arguments = new Object[0]; - @Nullable - private Object returnValue; + private @Nullable Object returnValue; Builder(InstrumentedMethod instrumentedMethod) { @@ -234,7 +230,7 @@ public Builder withArgument(@Nullable Object argument) { * @param arguments the invocation arguments * @return {@code this}, to facilitate method chaining */ - public Builder withArguments(@Nullable Object... arguments) { + public Builder withArguments(Object @Nullable ... arguments) { if (arguments != null) { this.arguments = arguments; } 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 375fdb2dbac5..3ea8f86a2460 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 @@ -20,8 +20,9 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -39,7 +40,10 @@ * @author Brian Clozel * @since 6.0 * @see InvocationsRecorderClassTransformer + * @deprecated as of 7.0 in favor of the {@code -XX:MissingRegistrationReportingMode=Warn} and + * {@code -XX:MissingRegistrationReportingMode=Exit} JVM flags with GraalVM. */ +@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/agent/package-info.java b/spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java index 1ea09811ae19..e19fa356b961 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java +++ b/spring-core-test/src/main/java/org/springframework/aot/agent/package-info.java @@ -1,9 +1,7 @@ /** * Support for recording method invocations relevant to {@link org.springframework.aot.hint.RuntimeHints} metadata. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.agent; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocationsAssert.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocationsAssert.java index febdd622183f..27dd18ad44c3 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocationsAssert.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/RuntimeHintsInvocationsAssert.java @@ -108,8 +108,8 @@ private ErrorMessageFactory errorMessageForInvocation(RecordedInvocation invocat private String formatStackTrace(Stream stackTraceElements) { return stackTraceElements - .map(f -> f.getClassName() + "#" + f.getMethodName() - + ", Line " + f.getLineNumber()).collect(Collectors.joining(System.lineSeparator())); + .map(f -> f.getClassName() + "#" + f.getMethodName() + ", Line " + + f.getLineNumber()).collect(Collectors.joining(System.lineSeparator())); } /** 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 50faf662d444..7184c9f90866 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 @@ -22,17 +22,20 @@ import org.springframework.aot.agent.RecordedInvocation; import org.springframework.aot.agent.RecordedInvocationsListener; import org.springframework.aot.agent.RecordedInvocationsPublisher; -import org.springframework.aot.agent.RuntimeHintsAgent; import org.springframework.aot.hint.RuntimeHints; import org.springframework.util.Assert; /** * Invocations relevant to {@link RuntimeHints} recorded during the execution of a block - * of code instrumented by the {@link RuntimeHintsAgent}. + * of code instrumented by the {@link org.springframework.aot.agent.RuntimeHintsAgent}. * * @author Brian Clozel * @since 6.0 + * @deprecated as of 7.0 in favor of the {@code -XX:MissingRegistrationReportingMode=Warn} and + * {@code -XX:MissingRegistrationReportingMode=Exit} JVM flags with GraalVM. */ +@Deprecated(since = "7.0", forRemoval = true) +@SuppressWarnings("removal") public final class RuntimeHintsRecorder { private final RuntimeHintsInvocationsListener listener; @@ -49,7 +52,7 @@ private RuntimeHintsRecorder() { */ public static synchronized RuntimeHintsInvocations record(Runnable action) { Assert.notNull(action, "Runnable action must not be null"); - Assert.state(RuntimeHintsAgent.isLoaded(), "RuntimeHintsAgent must be loaded in the current JVM"); + Assert.state(org.springframework.aot.agent.RuntimeHintsAgent.isLoaded(), "RuntimeHintsAgent must be loaded in the current JVM"); RuntimeHintsRecorder recorder = new RuntimeHintsRecorder(); RecordedInvocationsPublisher.addListener(recorder.listener); try { diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/agent/package-info.java b/spring-core-test/src/main/java/org/springframework/aot/test/agent/package-info.java index dc7ecdddfd68..ff84188ae02c 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/agent/package-info.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/agent/package-info.java @@ -1,9 +1,7 @@ /** * Testing support for the {@link org.springframework.aot.agent.RuntimeHintsAgent}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.test.agent; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core-test/src/main/java/org/springframework/aot/test/generate/package-info.java b/spring-core-test/src/main/java/org/springframework/aot/test/generate/package-info.java index 9235cf21d597..7f00527d6136 100644 --- a/spring-core-test/src/main/java/org/springframework/aot/test/generate/package-info.java +++ b/spring-core-test/src/main/java/org/springframework/aot/test/generate/package-info.java @@ -1,9 +1,7 @@ /** * Test support for core AOT classes. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.test.generate; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core-test/src/main/java/org/springframework/core/test/io/support/MockSpringFactoriesLoader.java b/spring-core-test/src/main/java/org/springframework/core/test/io/support/MockSpringFactoriesLoader.java index 70a6067a6a0d..1aa68de3c5dd 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/io/support/MockSpringFactoriesLoader.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/io/support/MockSpringFactoriesLoader.java @@ -24,8 +24,9 @@ import java.util.Map; import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.support.SpringFactoriesLoader; -import org.springframework.lang.Nullable; /** * Simple mock {@link SpringFactoriesLoader} implementation that can be used for testing @@ -67,9 +68,8 @@ protected MockSpringFactoriesLoader(@Nullable ClassLoader classLoader, @Override - @Nullable @SuppressWarnings("unchecked") - protected T instantiateFactory(String implementationName, Class type, + protected @Nullable T instantiateFactory(String implementationName, Class type, @Nullable ArgumentResolver argumentResolver, FailureHandler failureHandler) { if (implementationName.startsWith("!")) { Object implementation = this.implementations.get(implementationName); @@ -122,8 +122,8 @@ public void addInstance(Class factoryType, T... factoryInstances) { public void addInstance(String factoryType, T... factoryInstance) { List implementations = this.factories.computeIfAbsent(factoryType, key -> new ArrayList<>()); for (T factoryImplementation : factoryInstance) { - String reference = "!" + factoryType + ":" + factoryImplementation.getClass().getName() - + this.sequence.getAndIncrement(); + String reference = "!" + factoryType + ":" + factoryImplementation.getClass().getName() + + this.sequence.getAndIncrement(); implementations.add(reference); this.implementations.put(reference, factoryImplementation); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/io/support/package-info.java b/spring-core-test/src/main/java/org/springframework/core/test/io/support/package-info.java index 99ef46417165..d87f5a2b56fd 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/io/support/package-info.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/io/support/package-info.java @@ -1,9 +1,7 @@ /** * Test support classes for Spring's I/O support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.test.io.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/ClassFiles.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/ClassFiles.java index 595c920a3acc..711ed5d2e403 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/ClassFiles.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/ClassFiles.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An immutable collection of {@link ClassFile} instances. @@ -112,8 +112,7 @@ public boolean isEmpty() { * @param name the fully qualified name to find * @return a {@link ClassFile} instance or {@code null} */ - @Nullable - public ClassFile get(String name) { + public @Nullable ClassFile get(String name) { return this.files.get(name); } 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 fe728b22acfa..36bf8512109b 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 @@ -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 java.util.Enumeration; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link ClassLoader} implementation to support @@ -37,7 +37,7 @@ final class CompileWithForkedClassLoaderClassLoader extends ClassLoader { private final ClassLoader testClassLoader; - private Function classResourceLookup = name -> null; + private Function classResourceLookup = name -> null; public CompileWithForkedClassLoaderClassLoader(ClassLoader testClassLoader) { @@ -48,7 +48,7 @@ public CompileWithForkedClassLoaderClassLoader(ClassLoader testClassLoader) { // Invoked reflectively by DynamicClassLoader @SuppressWarnings("unused") - void setClassResourceLookup(Function classResourceLookup) { + void setClassResourceLookup(Function classResourceLookup) { this.classResourceLookup = classResourceLookup; } @@ -73,8 +73,7 @@ protected Class findClass(String name) throws ClassNotFoundException { return (bytes != null ? defineClass(name, bytes, 0, bytes.length, null) : super.findClass(name)); } - @Nullable - private byte[] findClassBytes(String name) { + private byte @Nullable [] findClassBytes(String name) { byte[] bytes = this.classResourceLookup.apply(name); if (bytes != null) { return bytes; @@ -98,8 +97,7 @@ protected Enumeration findResources(String name) throws IOException { } @Override - @Nullable - protected URL findResource(String name) { + protected @Nullable URL findResource(String name) { return this.testClassLoader.getResource(name); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/Compiled.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/Compiled.java index 1ef3175e4507..4125df1c4139 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/Compiled.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/Compiled.java @@ -21,7 +21,8 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -38,8 +39,7 @@ public class Compiled { private final ResourceFiles resourceFiles; - @Nullable - private List> compiledClasses; + private @Nullable List> compiledClasses; Compiled(ClassLoader classLoader, SourceFiles sourceFiles, ResourceFiles resourceFiles) { diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassFileObject.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassFileObject.java index cd395d4a46b2..de8ee3360f1e 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassFileObject.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassFileObject.java @@ -26,7 +26,7 @@ import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * In-memory {@link JavaFileObject} used to hold class bytecode. @@ -38,8 +38,7 @@ class DynamicClassFileObject extends SimpleJavaFileObject { private final String className; - @Nullable - private volatile byte[] bytes; + private volatile byte @Nullable [] bytes; DynamicClassFileObject(String className) { @@ -80,8 +79,7 @@ String getClassName() { return this.className; } - @Nullable - byte[] getBytes() { + byte @Nullable [] getBytes() { return this.bytes; } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java index c55d66401192..ed8f58d5a774 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicClassLoader.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,7 +29,8 @@ import java.util.function.Function; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -52,8 +53,7 @@ public class DynamicClassLoader extends ClassLoader { private final Map dynamicResourceFiles; - @Nullable - private final Method defineClassMethod; + private final @Nullable Method defineClassMethod; public DynamicClassLoader(ClassLoader parent, ClassFiles classFiles, ResourceFiles resourceFiles, @@ -71,7 +71,7 @@ public DynamicClassLoader(ClassLoader parent, ClassFiles classFiles, ResourceFil "setClassResourceLookup", Function.class); ReflectionUtils.makeAccessible(setClassResourceLookupMethod); ReflectionUtils.invokeMethod(setClassResourceLookupMethod, - getParent(), (Function) this::findClassBytes); + getParent(), (Function) this::findClassBytes); this.defineClassMethod = lookupMethod(parentClass, "defineDynamicClass", String.class, byte[].class, int.class, int.class); ReflectionUtils.makeAccessible(this.defineClassMethod); @@ -89,8 +89,7 @@ protected Class findClass(String name) throws ClassNotFoundException { return (clazz != null ? clazz : super.findClass(name)); } - @Nullable - private Class defineClass(String name, @Nullable byte[] bytes) { + private @Nullable Class defineClass(String name, byte @Nullable [] bytes) { if (bytes == null) { return null; } @@ -111,8 +110,7 @@ protected Enumeration findResources(String name) throws IOException { } @Override - @Nullable - protected URL findResource(String name) { + protected @Nullable URL findResource(String name) { if (name.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) { String className = ClassUtils.convertResourcePathToClassName(name.substring(0, name.length() - ClassUtils.CLASS_FILE_SUFFIX.length())); @@ -132,8 +130,7 @@ protected URL findResource(String name) { return super.findResource(name); } - @Nullable - private byte[] findClassBytes(String name) { + private byte @Nullable [] findClassBytes(String name) { ClassFile classFile = this.classFiles.get(name); if (classFile != null) { return classFile.getContent(); @@ -162,8 +159,7 @@ private static Method lookupMethod(Class target, String name, Class... par private static class SingletonEnumeration implements Enumeration { - @Nullable - private E element; + private @Nullable E element; SingletonEnumeration(@Nullable E element) { @@ -177,8 +173,7 @@ public boolean hasMoreElements() { } @Override - @Nullable - public E nextElement() { + public @Nullable E nextElement() { E next = this.element; this.element = null; return next; diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFile.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFile.java index bbc81923a7cf..f17d7b048f7f 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFile.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFile.java @@ -19,7 +19,8 @@ import java.io.IOException; import java.nio.charset.StandardCharsets; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFileAssert.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFileAssert.java index f59f33fb738b..dbbf3f79a1b0 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFileAssert.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFileAssert.java @@ -17,8 +17,7 @@ package org.springframework.core.test.tools; import org.assertj.core.api.AbstractAssert; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFiles.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFiles.java index 23b2d47ccd30..29c0f2e9710f 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFiles.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicFiles.java @@ -25,7 +25,7 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Internal class used by {@link SourceFiles} and {@link ResourceFiles} to @@ -83,8 +83,7 @@ boolean isEmpty() { return this.files.isEmpty(); } - @Nullable - F get(String path) { + @Nullable F get(String path) { return this.files.get(path); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicResourceFileObject.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicResourceFileObject.java index 5e09f46e5430..43a4bae34214 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicResourceFileObject.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/DynamicResourceFileObject.java @@ -26,7 +26,7 @@ import javax.tools.JavaFileObject; import javax.tools.SimpleJavaFileObject; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * In-memory {@link JavaFileObject} used to hold generated resource file contents. @@ -37,8 +37,7 @@ */ class DynamicResourceFileObject extends SimpleJavaFileObject { - @Nullable - private volatile byte[] bytes; + private volatile byte @Nullable [] bytes; DynamicResourceFileObject(String fileName) { @@ -73,8 +72,7 @@ private void closeOutputStream(byte[] bytes) { this.bytes = bytes; } - @Nullable - byte[] getBytes() { + byte @Nullable [] getBytes() { return this.bytes; } 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 1a53e922774b..8608e37a24b2 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. @@ -19,7 +19,7 @@ import java.util.Iterator; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An immutable collection of {@link ResourceFile} instances. @@ -81,11 +81,11 @@ public ResourceFiles and(Iterable resourceFiles) { /** * 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 @@ -115,8 +115,7 @@ public boolean isEmpty() { * @param path the path to find * @return a {@link ResourceFile} instance or {@code null} */ - @Nullable - public ResourceFile get(String path) { + public @Nullable ResourceFile get(String path) { return this.files.get(path); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java index 8db946050b88..02918a4bc698 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFile.java @@ -29,9 +29,9 @@ import com.thoughtworks.qdox.model.JavaClass; import com.thoughtworks.qdox.model.JavaSource; import org.assertj.core.api.AssertProvider; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.InputStreamSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.FileCopyUtils; diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFiles.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFiles.java index ad8bab385af9..1505a5412d6c 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFiles.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/SourceFiles.java @@ -20,7 +20,8 @@ import java.util.regex.Pattern; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** @@ -117,8 +118,7 @@ public boolean isEmpty() { * @param path the path to find * @return a {@link SourceFile} instance or {@code null} */ - @Nullable - public SourceFile get(String path) { + public @Nullable SourceFile get(String path) { return this.files.get(path); } diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java index bb77051b9e7f..2f51bfad7828 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/TestCompiler.java @@ -35,7 +35,7 @@ import javax.tools.StandardJavaFileManager; import javax.tools.ToolProvider; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility that can be used to dynamically compile and test Java source code. @@ -48,8 +48,7 @@ */ public final class TestCompiler { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final JavaCompiler compiler; @@ -299,8 +298,8 @@ public void compile(Consumer compiled) throws CompilationException { } private DynamicClassLoader compile() { - ClassLoader classLoaderToUse = (this.classLoader != null ? this.classLoader - : Thread.currentThread().getContextClassLoader()); + ClassLoader classLoaderToUse = (this.classLoader != null ? this.classLoader : + Thread.currentThread().getContextClassLoader()); List compilationUnits = this.sourceFiles.stream().map( DynamicJavaFileObject::new).toList(); StandardJavaFileManager standardFileManager = this.compiler.getStandardFileManager( diff --git a/spring-core-test/src/main/java/org/springframework/core/test/tools/package-info.java b/spring-core-test/src/main/java/org/springframework/core/test/tools/package-info.java index a541c066c4cb..750eb47a7152 100644 --- a/spring-core-test/src/main/java/org/springframework/core/test/tools/package-info.java +++ b/spring-core-test/src/main/java/org/springframework/core/test/tools/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for compiling and testing generated code. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.test.tools; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java index 48a9b68652e6..70a86c6f0dc8 100644 --- a/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java +++ b/spring-core-test/src/test/java/org/springframework/aot/agent/InstrumentedMethodTests.java @@ -61,32 +61,26 @@ void classForNameShouldMatchReflectionOnType() { } @Test - void classGetClassesShouldNotMatchReflectionOnType() { - hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses); - } - - @Test - void classGetClassesShouldMatchPublicClasses() { - hints.reflection().registerType(String.class, MemberCategory.PUBLIC_CLASSES); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses); + void classForNameShouldNotMatchWhenMissingReflectionOnType() { + RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_FORNAME) + .withArgument("java.lang.String").returnValue(String.class).build(); + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_FORNAME, invocation); } @Test - void classGetClassesShouldMatchDeclaredClasses() { - hints.reflection().registerType(String.class, MemberCategory.DECLARED_CLASSES); + void classGetClassesShouldMatchReflectionOnType() { + hints.reflection().registerType(String.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCLASSES, this.stringGetClasses); } @Test - void classGetDeclaredClassesShouldMatchDeclaredClassesHint() { - hints.reflection().registerType(String.class, MemberCategory.DECLARED_CLASSES); + void classGetDeclaredClassesShouldMatchReflectionOnType() { + hints.reflection().registerType(String.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCLASSES, this.stringGetDeclaredClasses); } @Test - void classGetDeclaredClassesShouldNotMatchPublicClassesHint() { - hints.reflection().registerType(String.class, MemberCategory.PUBLIC_CLASSES); + void classGetDeclaredClassesShouldNotMatchWhenMissingReflectionOnType() { assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCLASSES, this.stringGetDeclaredClasses); } @@ -124,20 +118,19 @@ public void setup() throws Exception { } @Test - void classGetConstructorShouldMatchIntrospectPublicConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); + void classGetConstructorShouldMatchTypeHint() { + hints.reflection().registerType(String.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } @Test - void classGetConstructorShouldMatchInvokePublicConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); + void classGetConstructorShouldNotMatchWhenMissingTypeHint() { + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } @Test - void classGetConstructorShouldMatchIntrospectDeclaredConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); + void classGetConstructorShouldMatchInvokePublicConstructorsHint() { + hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } @@ -147,13 +140,6 @@ void classGetConstructorShouldMatchInvokeDeclaredConstructorsHint() { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); } - @Test - void classGetConstructorShouldMatchIntrospectConstructorHint() { - hints.reflection().registerType(String.class,typeHint -> - typeHint.withConstructor(Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTOR, this.stringGetConstructor); - } - @Test void classGetConstructorShouldMatchInvokeConstructorHint() { hints.reflection().registerType(String.class, typeHint -> @@ -162,20 +148,19 @@ void classGetConstructorShouldMatchInvokeConstructorHint() { } @Test - void classGetConstructorsShouldMatchIntrospectPublicConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); + void classGetConstructorsShouldMatchTypeHint() { + hints.reflection().registerType(String.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); } @Test - void classGetConstructorsShouldMatchInvokePublicConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); + void classGetConstructorsShouldNotMatchWhemMissingTypeHint() { + assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); } @Test - void classGetConstructorsShouldMatchIntrospectDeclaredConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); + void classGetConstructorsShouldMatchInvokePublicConstructorsHint() { + hints.reflection().registerType(String.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); } @@ -186,36 +171,16 @@ void classGetConstructorsShouldMatchInvokeDeclaredConstructorsHint() { } @Test - void classGetConstructorsShouldNotMatchTypeReflectionHint() { + void classGetDeclaredConstructorShouldMatchTypeHint() { hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); - } - - @Test - void classGetConstructorsShouldNotMatchConstructorReflectionHint() throws Exception { - hints.reflection().registerConstructor(String.class.getConstructor(), ExecutableMode.INVOKE); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETCONSTRUCTORS, this.stringGetConstructors); - } - - @Test - void classGetDeclaredConstructorShouldMatchIntrospectDeclaredConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); } @Test - void classGetDeclaredConstructorShouldNotMatchIntrospectPublicConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); + void classGetDeclaredConstructorShouldNotMatchWhenMissingTypeHint() { assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); } - @Test - void classGetDeclaredConstructorShouldMatchIntrospectConstructorHint() { - hints.reflection().registerType(String.class, typeHint -> - typeHint.withConstructor(TypeReference.listOf(byte[].class, byte.class), ExecutableMode.INTROSPECT)); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); - } - @Test void classGetDeclaredConstructorShouldMatchInvokeConstructorHint() { hints.reflection().registerType(String.class, typeHint -> @@ -223,12 +188,6 @@ void classGetDeclaredConstructorShouldMatchInvokeConstructorHint() { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTOR, this.stringGetDeclaredConstructor); } - @Test - void classGetDeclaredConstructorsShouldMatchIntrospectDeclaredConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); - } - @Test void classGetDeclaredConstructorsShouldMatchInvokeDeclaredConstructorsHint() { hints.reflection().registerType(String.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); @@ -236,20 +195,13 @@ void classGetDeclaredConstructorsShouldMatchInvokeDeclaredConstructorsHint() { } @Test - void classGetDeclaredConstructorsShouldNotMatchIntrospectPublicConstructorsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); - } - - @Test - void classGetDeclaredConstructorsShouldNotMatchTypeReflectionHint() { + void classGetDeclaredConstructorsShouldMatchTypeReflectionHint() { hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); } @Test - void classGetDeclaredConstructorsShouldNotMatchConstructorReflectionHint() throws Exception { - hints.reflection().registerConstructor(String.class.getConstructor(), ExecutableMode.INVOKE); + void classGetDeclaredConstructorsShouldNotMatchWhenMissingTypeReflectionHint() { assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDCONSTRUCTORS, this.stringGetDeclaredConstructors); } @@ -262,15 +214,6 @@ void constructorNewInstanceShouldMatchInvokeHintOnConstructor() throws NoSuchMet assertThatInvocationMatches(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE, invocation); } - @Test - void constructorNewInstanceShouldNotMatchIntrospectHintOnConstructor() throws NoSuchMethodException { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE) - .onInstance(String.class.getConstructor()).returnValue("").build(); - hints.reflection().registerType(String.class, typeHint -> - typeHint.withConstructor(Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CONSTRUCTOR_NEWINSTANCE, invocation); - } - } @Nested @@ -295,22 +238,8 @@ void setup() throws Exception { } @Test - void classGetDeclaredMethodShouldMatchIntrospectDeclaredMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); - } - - @Test - void classGetDeclaredMethodShouldNotMatchIntrospectPublicMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); - } - - @Test - void classGetDeclaredMethodShouldMatchIntrospectMethodHint() { - List parameterTypes = TypeReference.listOf(int.class, float.class); - hints.reflection().registerType(String.class, typeHint -> - typeHint.withMethod("scale", parameterTypes, ExecutableMode.INTROSPECT)); + void classGetDeclaredMethodShouldMatchTypeReflectionHint() { + hints.reflection().registerType(String.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); } @@ -322,12 +251,6 @@ void classGetDeclaredMethodShouldMatchInvokeMethodHint() { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHOD, this.stringGetScaleMethod); } - @Test - void classGetDeclaredMethodsShouldMatchIntrospectDeclaredMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); - } - @Test void classGetDeclaredMethodsShouldMatchInvokeDeclaredMethodsHint() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETDECLAREDMETHODS).onInstance(String.class).build(); @@ -336,32 +259,8 @@ void classGetDeclaredMethodsShouldMatchInvokeDeclaredMethodsHint() { } @Test - void classGetDeclaredMethodsShouldNotMatchIntrospectPublicMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); - } - - @Test - void classGetDeclaredMethodsShouldNotMatchTypeReflectionHint() { + void classGetMethodsShouldMatchTypeReflectionHint() { hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); - } - - @Test - void classGetDeclaredMethodsShouldNotMatchMethodReflectionHint() throws Exception { - hints.reflection().registerMethod(String.class.getMethod("toString"), ExecutableMode.INVOKE); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDMETHODS, this.stringGetScaleMethod); - } - - @Test - void classGetMethodsShouldMatchIntrospectDeclaredMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); - } - - @Test - void classGetMethodsShouldMatchIntrospectPublicMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @@ -379,25 +278,13 @@ void classGetMethodsShouldMatchInvokePublicMethodsHint() { @Test void classGetMethodsShouldNotMatchForWrongType() { - hints.reflection().registerType(Integer.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + hints.reflection().registerType(Integer.class); assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); } @Test - void classGetMethodsShouldNotMatchTypeReflectionHint() { + void classGetMethodShouldMatchReflectionTypeHint() { hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); - } - - @Test - void classGetMethodsShouldNotMatchMethodReflectionHint() throws Exception { - hints.reflection().registerMethod(String.class.getMethod("toString"), ExecutableMode.INVOKE); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHODS, this.stringGetMethods); - } - - @Test - void classGetMethodShouldMatchIntrospectPublicMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @@ -407,25 +294,6 @@ void classGetMethodShouldMatchInvokePublicMethodsHint() { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } - @Test - void classGetMethodShouldNotMatchIntrospectDeclaredMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); - } - - @Test - void classGetMethodShouldNotMatchInvokeDeclaredMethodsHint() { - hints.reflection().registerType(String.class, MemberCategory.INVOKE_DECLARED_METHODS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); - } - - @Test - void classGetMethodShouldMatchIntrospectMethodHint() { - hints.reflection().registerType(String.class, typeHint -> - typeHint.withMethod("toString", Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); - } - @Test void classGetMethodShouldMatchInvokeMethodHint() { hints.reflection().registerType(String.class, typeHint -> @@ -433,21 +301,9 @@ void classGetMethodShouldMatchInvokeMethodHint() { assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } - @Test - void classGetMethodShouldNotMatchIntrospectPublicMethodsHintWhenPrivate() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod); - } - - @Test - void classGetMethodShouldMatchIntrospectDeclaredMethodsHintWhenPrivate() { - hints.reflection().registerType(String.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetScaleMethod); - } - @Test void classGetMethodShouldNotMatchForWrongType() { - hints.reflection().registerType(Integer.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); + hints.reflection().registerType(Integer.class); assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETMETHOD, this.stringGetToStringMethod); } @@ -461,11 +317,10 @@ void methodInvokeShouldMatchInvokeHintOnMethod() throws NoSuchMethodException { } @Test - void methodInvokeShouldNotMatchIntrospectHintOnMethod() throws NoSuchMethodException { + void methodInvokeShouldNotMatchReflectionTypeHint() throws NoSuchMethodException { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.METHOD_INVOKE) .onInstance(String.class.getMethod("toString")).withArguments("", new Object[0]).build(); - hints.reflection().registerType(String.class, typeHint -> - typeHint.withMethod("toString", Collections.emptyList(), ExecutableMode.INTROSPECT)); + hints.reflection().registerType(String.class); assertThatInvocationDoesNotMatch(InstrumentedMethod.METHOD_INVOKE, invocation); } @@ -496,17 +351,11 @@ void setup() throws Exception { } @Test - void classGetDeclaredFieldShouldMatchDeclaredFieldsHint() { - hints.reflection().registerType(String.class, MemberCategory.DECLARED_FIELDS); + void classGetDeclaredFieldShouldMatchTypeReflectionHint() { + hints.reflection().registerType(String.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELD, this.stringGetDeclaredField); } - @Test - void classGetDeclaredFieldShouldNotMatchPublicFieldsHint() { - hints.reflection().registerType(String.class, typeHint -> typeHint.withMembers(MemberCategory.PUBLIC_FIELDS)); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELD, this.stringGetDeclaredField); - } - @Test void classGetDeclaredFieldShouldMatchFieldHint() { hints.reflection().registerType(String.class, typeHint -> typeHint.withField("value")); @@ -514,41 +363,23 @@ void classGetDeclaredFieldShouldMatchFieldHint() { } @Test - void classGetDeclaredFieldsShouldMatchDeclaredFieldsHint() { - hints.reflection().registerType(String.class, MemberCategory.DECLARED_FIELDS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); - } - - @Test - void classGetDeclaredFieldsShouldNotMatchPublicFieldsHint() { - hints.reflection().registerType(String.class, MemberCategory.PUBLIC_FIELDS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); - } - - @Test - void classGetDeclaredFieldsShouldNotMatchTypeHint() { + void classGetDeclaredFieldsShouldMatchTypeReflectionHint() { hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); } @Test - void classGetDeclaredFieldsShouldNotMatchFieldHint() throws Exception { + void classGetDeclaredFieldsShouldMatchFieldHint() throws Exception { hints.reflection().registerField(String.class.getDeclaredField("value")); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETDECLAREDFIELDS, this.stringGetDeclaredFields); } @Test - void classGetFieldShouldMatchPublicFieldsHint() { - hints.reflection().registerType(PublicField.class, MemberCategory.PUBLIC_FIELDS); + void classGetFieldShouldMatchTypeReflectionHint() { + hints.reflection().registerType(PublicField.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELD, this.getPublicField); } - @Test - void classGetFieldShouldNotMatchDeclaredFieldsHint() { - hints.reflection().registerType(PublicField.class, MemberCategory.DECLARED_FIELDS); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELD, this.getPublicField); - } - @Test void classGetFieldShouldMatchFieldHint() { hints.reflection().registerType(PublicField.class, typeHint -> typeHint.withField("field")); @@ -559,53 +390,30 @@ void classGetFieldShouldMatchFieldHint() { void classGetFieldShouldNotMatchPublicFieldsHintWhenPrivate() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) .onInstance(String.class).withArgument("value").returnValue(null).build(); - hints.reflection().registerType(String.class, MemberCategory.PUBLIC_FIELDS); + hints.reflection().registerType(String.class); assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELD, invocation); } - @Test - void classGetFieldShouldMatchDeclaredFieldsHintWhenPrivate() throws NoSuchFieldException { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) - .onInstance(String.class).withArgument("value").returnValue(String.class.getDeclaredField("value")).build(); - hints.reflection().registerType(String.class, MemberCategory.DECLARED_FIELDS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELD, invocation); - } - @Test void classGetFieldShouldNotMatchForWrongType() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELD) .onInstance(String.class).withArgument("value").returnValue(null).build(); - hints.reflection().registerType(Integer.class, MemberCategory.DECLARED_FIELDS); + hints.reflection().registerType(Integer.class); assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELD, invocation); } @Test - void classGetFieldsShouldMatchPublicFieldsHint() { - RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELDS) - .onInstance(PublicField.class).build(); - hints.reflection().registerType(PublicField.class, MemberCategory.PUBLIC_FIELDS); - assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, invocation); - } - - @Test - void classGetFieldsShouldMatchDeclaredFieldsHint() { + void classGetFieldsShouldMatchReflectionHint() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETFIELDS) .onInstance(PublicField.class).build(); - hints.reflection().registerType(PublicField.class, MemberCategory.DECLARED_FIELDS); + hints.reflection().registerType(PublicField.class); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, invocation); } - @Test - void classGetFieldsShouldNotMatchTypeHint() { + void classGetFieldsShouldMatchTypeHint() { hints.reflection().registerType(String.class); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELDS, this.stringGetFields); - } - - @Test - void classGetFieldsShouldNotMatchFieldHint() throws Exception { - hints.reflection().registerField(String.class.getDeclaredField("value")); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETFIELDS, this.stringGetFields); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETFIELDS, this.stringGetFields); } } @@ -634,7 +442,7 @@ void resourceBundleGetBundleShouldNotMatchBundleNameHintWhenWrongName() { void classGetResourceShouldMatchResourcePatternWhenAbsolute() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) .onInstance(InstrumentedMethodTests.class).withArgument("/some/path/resource.txt").build(); - hints.resources().registerPattern("some/*"); + hints.resources().registerPattern("some/**"); assertThatInvocationMatches(InstrumentedMethod.CLASS_GETRESOURCE, invocation); } @@ -655,11 +463,11 @@ void classGetResourceShouldNotMatchResourcePatternWhenInvalid() { } @Test - void classGetResourceShouldNotMatchResourcePatternWhenExcluded() { + void classGetResourceShouldMatchWhenGlobPattern() { RecordedInvocation invocation = RecordedInvocation.of(InstrumentedMethod.CLASS_GETRESOURCE) .onInstance(InstrumentedMethodTests.class).withArgument("/some/path/resource.txt").build(); - hints.resources().registerPattern(resourceHint -> resourceHint.includes("some/*").excludes("some/path/*")); - assertThatInvocationDoesNotMatch(InstrumentedMethod.CLASS_GETRESOURCE, invocation); + hints.resources().registerPattern(resourceHint -> resourceHint.includes("some/**")); + assertThatInvocationMatches(InstrumentedMethod.CLASS_GETRESOURCE, invocation); } } diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index a08d0e166723..5ac17dd889dd 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -2,7 +2,7 @@ import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar import org.springframework.build.shadow.ShadowSource plugins { - id 'me.champeau.mrjar' + id 'org.springframework.build.multiReleaseJar' } description = "Spring Core" @@ -11,7 +11,7 @@ apply plugin: "kotlin" apply plugin: "kotlinx-serialization" multiRelease { - targetVersions 17, 21 + releaseVersions 21, 24 } def javapoetVersion = "1.13.0" @@ -20,45 +20,46 @@ def objenesisVersion = "3.4" configurations { java21Api.extendsFrom(api) java21Implementation.extendsFrom(implementation) + java24Api.extendsFrom(api) javapoet objenesis graalvm } -task javapoetRepackJar(type: ShadowJar) { +tasks.register('javapoetRepackJar', ShadowJar) { archiveBaseName = 'spring-javapoet-repack' archiveVersion = javapoetVersion configurations = [project.configurations.javapoet] relocate('com.squareup.javapoet', 'org.springframework.javapoet') } -task javapoetSource(type: ShadowSource) { +tasks.register('javapoetSource', ShadowSource) { configurations = [project.configurations.javapoet] relocate('com.squareup.javapoet', 'org.springframework.javapoet') outputDirectory = file("build/shadow-source/javapoet") } -task javapoetSourceJar(type: Jar) { +tasks.register('javapoetSourceJar', Jar) { archiveBaseName = 'spring-javapoet-repack' archiveVersion = javapoetVersion archiveClassifier = 'sources' from javapoetSource } -task objenesisRepackJar(type: ShadowJar) { +tasks.register('objenesisRepackJar', ShadowJar) { archiveBaseName = 'spring-objenesis-repack' archiveVersion = objenesisVersion configurations = [project.configurations.objenesis] relocate('org.objenesis', 'org.springframework.objenesis') } -task objenesisSource(type: ShadowSource) { +tasks.register('objenesisSource', ShadowSource) { configurations = [project.configurations.objenesis] relocate('org.objenesis', 'org.springframework.objenesis') outputDirectory = file("build/shadow-source/objenesis") } -task objenesisSourceJar(type: Jar) { +tasks.register('objenesisSourceJar', Jar) { archiveBaseName = 'spring-objenesis-repack' archiveVersion = objenesisVersion archiveClassifier = 'sources' @@ -70,12 +71,13 @@ dependencies { objenesis("org.objenesis:objenesis:${objenesisVersion}@jar") api(files(javapoetRepackJar)) api(files(objenesisRepackJar)) - api(project(":spring-jcl")) + api("commons-logging:commons-logging") + api("org.jspecify:jspecify") + compileOnly("com.google.code.findbugs:jsr305") compileOnly("io.projectreactor.tools:blockhound") compileOnly("org.graalvm.sdk:graal-sdk") optional("io.micrometer:context-propagation") optional("io.netty:netty-buffer") - optional("io.netty:netty5-buffer") optional("io.projectreactor:reactor-core") optional("io.reactivex.rxjava3:rxjava") optional("io.smallrye.reactive:mutiny") @@ -86,7 +88,6 @@ dependencies { optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") optional("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") - testFixturesImplementation("com.google.code.findbugs:jsr305") testFixturesImplementation("io.projectreactor:reactor-test") testFixturesImplementation("org.assertj:assertj-core") testFixturesImplementation("org.junit.platform:junit-platform-launcher") @@ -104,6 +105,7 @@ dependencies { testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") testImplementation("org.mockito:mockito-core") + testImplementation("com.networknt:json-schema-validator"); testImplementation("org.skyscreamer:jsonassert") testImplementation("org.xmlunit:xmlunit-assertj") testImplementation("org.xmlunit:xmlunit-matchers") diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java index 48c88041e484..dc617405ec4d 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ClassNameGenerator.java @@ -20,8 +20,9 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.ClassName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; diff --git a/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java b/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java index 3018c6407cfc..2508151e03dd 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/DefaultMethodReference.java @@ -21,11 +21,12 @@ import javax.lang.model.element.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,8 +40,7 @@ public class DefaultMethodReference implements MethodReference { private final MethodSpec method; - @Nullable - private final ClassName declaringClass; + private final @Nullable ClassName declaringClass; public DefaultMethodReference(MethodSpec method, @Nullable ClassName declaringClass) { @@ -102,8 +102,8 @@ protected void addArguments(CodeBlock.Builder code, ArgumentCodeGenerator argume TypeName argumentType = argumentTypes[i]; CodeBlock argumentCode = argumentCodeGenerator.generateCode(argumentType); if (argumentCode == null) { - throw new IllegalArgumentException("Could not generate code for " + this - + ": parameter " + i + " of type " + argumentType + " is not supported"); + throw new IllegalArgumentException("Could not generate code for " + this + + ": parameter " + i + " of type " + argumentType + " is not supported"); } arguments.add(argumentCode); } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java index 2b1caa6d1365..23c7a81920c4 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClass.java @@ -21,10 +21,11 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.ClassName; import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ */ public final class GeneratedClass { - @Nullable - private final GeneratedClass enclosingClass; + private final @Nullable GeneratedClass enclosingClass; private final ClassName name; @@ -98,8 +98,7 @@ private String generateSequencedMethodName(MethodName name) { * instance represents a top-level class. * @return the enclosing generated class, if any */ - @Nullable - public GeneratedClass getEnclosingClass() { + public @Nullable GeneratedClass getEnclosingClass() { return this.enclosingClass; } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java index 5e057001bb2d..e6aad3d286c5 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedClasses.java @@ -23,9 +23,10 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.ClassName; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java index 98979191a81a..4fb0e6324939 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedFiles.java @@ -18,9 +18,10 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.InputStreamSource; import org.springframework.javapoet.JavaFile; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.StringUtils; @@ -192,9 +193,9 @@ private static String getClassNamePath(String className) { private static void validatePackage(String packageName, String className) { if (!StringUtils.hasLength(packageName)) { - throw new IllegalArgumentException("Could not add '" + className + "', " - + "processing classes in the default package is not supported. " - + "Did you forget to add a package statement?"); + throw new IllegalArgumentException("Could not add '" + className + "', " + + "processing classes in the default package is not supported. " + + "Did you forget to add a package statement?"); } } @@ -264,8 +265,7 @@ public boolean exists() { * Return an {@link InputStreamSource} for the content of the file or * {@code null} if the file does not exist. */ - @Nullable - public InputStreamSource getContent() { + public @Nullable InputStreamSource getContent() { return (exists() ? this.existingContent.get() : null); } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedTypeReference.java b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedTypeReference.java index 9e2ec90886b3..54bb19bcbb41 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/GeneratedTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/GeneratedTypeReference.java @@ -16,10 +16,11 @@ package org.springframework.aot.generate; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.AbstractTypeReference; import org.springframework.aot.hint.TypeReference; import org.springframework.javapoet.ClassName; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ private GeneratedTypeReference(ClassName className) { this.className = className; } - @Nullable - private static GeneratedTypeReference safeCreate(@Nullable ClassName className) { + private static @Nullable GeneratedTypeReference safeCreate(@Nullable ClassName className) { return (className != null ? new GeneratedTypeReference(className) : null); } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java b/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java index b79a0b3318bc..7e82efa15c6a 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/InMemoryGeneratedFiles.java @@ -23,8 +23,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.InputStreamSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.function.ThrowingConsumer; @@ -64,8 +65,7 @@ public Map getGeneratedFiles(Kind kind) { * @return the file content or {@code null} if no file could be found * @throws IOException on read error */ - @Nullable - public String getGeneratedFileContent(Kind kind, String path) throws IOException { + public @Nullable String getGeneratedFileContent(Kind kind, String path) throws IOException { InputStreamSource source = getGeneratedFile(kind, path); if (source != null) { return new String(source.getInputStream().readAllBytes(), StandardCharsets.UTF_8); @@ -79,8 +79,7 @@ public String getGeneratedFileContent(Kind kind, String path) throws IOException * @param path the path of the file * @return the file source or {@code null} if no file could be found */ - @Nullable - public InputStreamSource getGeneratedFile(Kind kind, String path) { + public @Nullable InputStreamSource getGeneratedFile(Kind kind, String path) { Assert.notNull(kind, "'kind' must not be null"); Assert.hasLength(path, "'path' must not be empty"); Map paths = this.files.get(kind); diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java index 6a4ff72f6f97..7630055f176d 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodName.java @@ -19,7 +19,8 @@ import java.util.Arrays; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.java b/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.java index a33573d74fd3..32027c7189f0 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/MethodReference.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,10 +18,11 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.ClassName; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.TypeName; -import org.springframework.lang.Nullable; /** * A reference to a method with convenient code generation for @@ -74,8 +75,7 @@ interface ArgumentCodeGenerator { * @param argumentType the argument type * @return the code for this argument, or {@code null} */ - @Nullable - CodeBlock generateCode(TypeName argumentType); + @Nullable CodeBlock generateCode(TypeName argumentType); /** * Factory method that returns an {@link ArgumentCodeGenerator} that @@ -106,7 +106,7 @@ static ArgumentCodeGenerator of(Class argumentType, String argumentCode) { * @param function the resolver function * @return a new {@link ArgumentCodeGenerator} instance backed by the function */ - static ArgumentCodeGenerator from(Function function) { + static ArgumentCodeGenerator from(Function function) { return function::apply; } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java index dead0694e49d..2faba073c194 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerationException.java @@ -16,7 +16,7 @@ package org.springframework.aot.generate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown when value code generation fails. @@ -27,8 +27,7 @@ @SuppressWarnings("serial") public class ValueCodeGenerationException extends RuntimeException { - @Nullable - private final Object value; + private final @Nullable Object value; protected ValueCodeGenerationException(String message, @Nullable Object value, @Nullable Throwable cause) { @@ -54,8 +53,7 @@ private static String buildErrorMessage(@Nullable Object value) { /** * Return the value that failed to be generated. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java index b30b1acff152..4699261f1668 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGenerator.java @@ -20,8 +20,9 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.javapoet.CodeBlock; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -41,8 +42,7 @@ public final class ValueCodeGenerator { private final List delegates; - @Nullable - private final GeneratedMethods generatedMethods; + private final @Nullable GeneratedMethods generatedMethods; private ValueCodeGenerator(List delegates, @Nullable GeneratedMethods generatedMethods) { @@ -128,8 +128,7 @@ public CodeBlock generateCode(@Nullable Object value) { * {@code null} if no specific scope is set. * @return the generated methods to use for code generation */ - @Nullable - public GeneratedMethods getGeneratedMethods() { + public @Nullable GeneratedMethods getGeneratedMethods() { return this.generatedMethods; } @@ -149,8 +148,7 @@ public interface Delegate { * @return the code that represents the specified value or {@code null} if * the specified value is not supported. */ - @Nullable - CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value); + @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value); } } diff --git a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java index 86904a0c30d3..6606c636282a 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/ValueCodeGeneratorDelegates.java @@ -30,11 +30,12 @@ import java.util.TreeSet; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.ValueCodeGenerator.Delegate; import org.springframework.core.ResolvableType; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.CodeBlock.Builder; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -94,8 +95,7 @@ protected CollectionDelegate(Class collectionType, CodeBlock emptyResult) { @Override @SuppressWarnings("unchecked") - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (this.collectionType.isInstance(value)) { T collection = (T) value; if (collection.isEmpty()) { @@ -136,8 +136,7 @@ public static class MapDelegate implements Delegate { private static final CodeBlock EMPTY_RESULT = CodeBlock.of("$T.emptyMap()", Collections.class); @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object value) { if (value instanceof Map map) { if (map.isEmpty()) { return EMPTY_RESULT; @@ -154,8 +153,7 @@ public CodeBlock generateCode(ValueCodeGenerator valueCodeGenerator, Object valu * @return the code that represents the specified map or {@code null} if * the specified map is not supported. */ - @Nullable - protected CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { + protected @Nullable CodeBlock generateMapCode(ValueCodeGenerator valueCodeGenerator, Map map) { map = orderForCodeConsistency(map); boolean useOfEntries = map.size() > 10; CodeBlock.Builder code = CodeBlock.builder(); @@ -209,8 +207,7 @@ private static class PrimitiveDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { if (value instanceof Boolean || value instanceof Integer) { return CodeBlock.of("$L", value); } @@ -240,8 +237,8 @@ private String escape(char ch) { if (escaped != null) { return escaped; } - return (!Character.isISOControl(ch)) ? Character.toString(ch) - : String.format("\\u%04x", (int) ch); + return (!Character.isISOControl(ch)) ? Character.toString(ch) : + String.format("\\u%04x", (int) ch); } } @@ -252,8 +249,7 @@ private String escape(char ch) { private static class StringDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { if (value instanceof String) { return CodeBlock.of("$S", value); } @@ -268,8 +264,7 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { private static class CharsetDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { if (value instanceof Charset charset) { return CodeBlock.of("$T.forName($S)", Charset.class, charset.name()); } @@ -284,8 +279,7 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { private static class EnumDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { if (value instanceof Enum enumValue) { return CodeBlock.of("$T.$L", enumValue.getDeclaringClass(), enumValue.name()); @@ -301,8 +295,7 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { private static class ClassDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { if (value instanceof Class clazz) { return CodeBlock.of("$T.class", ClassUtils.getUserClass(clazz)); } @@ -317,8 +310,7 @@ public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { private static class ResolvableTypeDelegate implements Delegate { @Override - @Nullable - public CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { + public @Nullable CodeBlock generateCode(ValueCodeGenerator codeGenerator, Object value) { if (value instanceof ResolvableType resolvableType) { return generateCode(resolvableType, false); } @@ -360,8 +352,7 @@ private static CodeBlock generateCodeWithGenerics(ResolvableType target, Class elements = Arrays.stream(ObjectUtils.toObjectArray(value)) .map(codeGenerator::generateCode); diff --git a/spring-core/src/main/java/org/springframework/aot/generate/package-info.java b/spring-core/src/main/java/org/springframework/aot/generate/package-info.java index c6c380a8e2c4..b85a083bf375 100644 --- a/spring-core/src/main/java/org/springframework/aot/generate/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/generate/package-info.java @@ -2,9 +2,7 @@ * Support classes for components that contribute generated code equivalent to a * runtime behavior. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.generate; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 77c78341674f..a70ecfcd6c68 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 @@ -18,7 +18,7 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Base {@link TypeReference} implementation that ensures consistent behaviour @@ -34,8 +34,7 @@ public abstract class AbstractTypeReference implements TypeReference { private final String simpleName; - @Nullable - private final TypeReference enclosingType; + private final @Nullable TypeReference enclosingType; protected AbstractTypeReference(String packageName, String simpleName, @Nullable TypeReference enclosingType) { @@ -63,9 +62,8 @@ public String getSimpleName() { return this.simpleName; } - @Nullable @Override - public TypeReference getEnclosingType() { + public @Nullable TypeReference getEnclosingType() { return this.enclosingType; } 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 280e02a290fd..f5620a683275 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 @@ -29,13 +29,13 @@ import kotlin.jvm.JvmClassMappingKt; import kotlin.reflect.KClass; +import org.jspecify.annotations.Nullable; import org.springframework.core.KotlinDetector; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -100,7 +100,7 @@ private void registerReflectionHints(ReflectionHints hints, Set seen, Type typeHint.withMembers(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS); } - typeHint.withMembers(MemberCategory.DECLARED_FIELDS, + typeHint.withMembers(MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); for (Method method : clazz.getMethods()) { String methodName = method.getName(); @@ -108,8 +108,8 @@ private void registerReflectionHints(ReflectionHints hints, Set seen, Type registerPropertyHints(hints, seen, method, 0); } else if ((methodName.startsWith("get") && method.getParameterCount() == 0 && method.getReturnType() != void.class) || - (methodName.startsWith("is") && method.getParameterCount() == 0 - && ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()) == Boolean.class)) { + (methodName.startsWith("is") && method.getParameterCount() == 0 && + ClassUtils.resolvePrimitiveIfNecessary(method.getReturnType()) == Boolean.class)) { registerPropertyHints(hints, seen, method, -1); } } @@ -120,8 +120,6 @@ else if ((methodName.startsWith("get") && method.getParameterCount() == 0 && met if (KotlinDetector.isKotlinType(clazz)) { KotlinDelegate.registerComponentHints(hints, clazz); registerKotlinSerializationHints(hints, clazz); - // For Kotlin reflection - typeHint.withMembers(MemberCategory.INTROSPECT_DECLARED_METHODS); } }); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ConditionalHint.java b/spring-core/src/main/java/org/springframework/aot/hint/ConditionalHint.java index c9784ef592c2..a19bc26bf6da 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ConditionalHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ConditionalHint.java @@ -16,7 +16,8 @@ package org.springframework.aot.hint; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** @@ -33,8 +34,7 @@ public interface ConditionalHint { * {@code null} if this hint should always been applied. * @return the reachable type, if any */ - @Nullable - TypeReference getReachableType(); + @Nullable TypeReference getReachableType(); /** * Whether the condition described for this hint is met. If it is not, diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java index 5d0c3a4d26dc..265377497690 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java @@ -24,7 +24,8 @@ import java.util.function.Consumer; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -113,8 +114,7 @@ public static class Builder { private final List parameterTypes; - @Nullable - private ExecutableMode mode; + private @Nullable ExecutableMode mode; Builder(String name, List parameterTypes) { diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java index 7082859e7501..c4e0a03de017 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ExecutableMode.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,7 +18,7 @@ import java.lang.reflect.Executable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represent the need of reflection for a given {@link Executable}. @@ -30,7 +30,10 @@ public enum ExecutableMode { /** * Only retrieving the {@link Executable} and its metadata is required. + * @deprecated with no replacement since introspection is included + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. */ + @Deprecated(since= "7.0", forRemoval = true) INTROSPECT, /** diff --git a/spring-core/src/main/java/org/springframework/aot/hint/JavaSerializationHint.java b/spring-core/src/main/java/org/springframework/aot/hint/JavaSerializationHint.java index 8f41d18f51bc..46bdf6c091f1 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/JavaSerializationHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/JavaSerializationHint.java @@ -20,7 +20,7 @@ import java.io.Serializable; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A hint that describes the need for Java serialization at runtime. @@ -32,8 +32,7 @@ public final class JavaSerializationHint implements ConditionalHint { private final TypeReference type; - @Nullable - private final TypeReference reachableType; + private final @Nullable TypeReference reachableType; JavaSerializationHint(Builder builder) { @@ -51,8 +50,7 @@ public TypeReference getType() { } @Override - @Nullable - public TypeReference getReachableType() { + public @Nullable TypeReference getReachableType() { return this.reachableType; } @@ -75,8 +73,7 @@ public static class Builder { private final TypeReference type; - @Nullable - private TypeReference reachableType; + private @Nullable TypeReference reachableType; Builder(TypeReference type) { this.type = type; diff --git a/spring-core/src/main/java/org/springframework/aot/hint/JdkProxyHint.java b/spring-core/src/main/java/org/springframework/aot/hint/JdkProxyHint.java index 4310ab5a4f0b..b9ddfec42582 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/JdkProxyHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/JdkProxyHint.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A hint that describes the need for a JDK interface-based {@link Proxy}. @@ -35,8 +35,7 @@ public final class JdkProxyHint implements ConditionalHint { private final List proxiedInterfaces; - @Nullable - private final TypeReference reachableType; + private final @Nullable TypeReference reachableType; private JdkProxyHint(Builder builder) { @@ -71,9 +70,8 @@ public List getProxiedInterfaces() { return this.proxiedInterfaces; } - @Nullable @Override - public TypeReference getReachableType() { + public @Nullable TypeReference getReachableType() { return this.reachableType; } @@ -97,8 +95,7 @@ public static class Builder { private final LinkedList proxiedInterfaces; - @Nullable - private TypeReference reachableType; + private @Nullable TypeReference reachableType; Builder() { this.proxiedInterfaces = new LinkedList<>(); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java b/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java index 4edd8a9179fd..072a388c0496 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/MemberCategory.java @@ -33,35 +33,64 @@ public enum MemberCategory { /** * A category that represents reflective field access on public {@linkplain Field fields}. + * @deprecated in favor of {@link #ACCESS_PUBLIC_FIELDS} with similar semantics. * @see Field#get(Object) * @see Field#set(Object, Object) */ + @Deprecated(since = "7.0", forRemoval = true) PUBLIC_FIELDS, /** * A category that represents reflective field access on * {@linkplain Class#getDeclaredFields() declared fields}: all fields defined by the * class but not inherited fields. + * @deprecated in favor of {@link #ACCESS_DECLARED_FIELDS} with similar semantics. * @see Class#getDeclaredFields() * @see Field#get(Object) * @see Field#set(Object, Object) */ + @Deprecated(since = "7.0", forRemoval = true) DECLARED_FIELDS, + /** + * A category that represents reflective field access on public {@linkplain Field fields}.. + * @see Field#get(Object) + * @see Field#set(Object, Object) + * @since 7.0 + */ + ACCESS_PUBLIC_FIELDS, + + /** + * A category that represents reflective field access on + * {@linkplain Class#getDeclaredFields() declared fields}: all fields defined by the + * class but not inherited fields. + * @see Class#getDeclaredFields() + * @see Field#get(Object) + * @see Field#set(Object, Object) + * @since 7.0 + */ + ACCESS_DECLARED_FIELDS, + /** * A category that defines public {@linkplain Constructor constructors} can * be introspected but not invoked. + * @deprecated with no replacement since introspection is included + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. * @see Class#getConstructors() * @see ExecutableMode#INTROSPECT */ + @Deprecated(since = "7.0", forRemoval = true) INTROSPECT_PUBLIC_CONSTRUCTORS, /** * A category that defines {@linkplain Class#getDeclaredConstructors() all * constructors} can be introspected but not invoked. + * @deprecated with no replacement since introspection is included + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. * @see Class#getDeclaredConstructors() * @see ExecutableMode#INTROSPECT */ + @Deprecated(since = "7.0", forRemoval = true) INTROSPECT_DECLARED_CONSTRUCTORS, /** @@ -83,17 +112,23 @@ public enum MemberCategory { /** * A category that defines public {@linkplain Method methods}, including * inherited ones, can be introspected but not invoked. + * @deprecated with no replacement since introspection is added by default + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. * @see Class#getMethods() * @see ExecutableMode#INTROSPECT */ + @Deprecated(since = "7.0", forRemoval = true) INTROSPECT_PUBLIC_METHODS, /** * A category that defines {@linkplain Class#getDeclaredMethods() all * methods}, excluding inherited ones, can be introspected but not invoked. + * @deprecated with no replacement since introspection is added by default + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. * @see Class#getDeclaredMethods() * @see ExecutableMode#INTROSPECT */ + @Deprecated(since = "7.0", forRemoval = true) INTROSPECT_DECLARED_METHODS, /** @@ -118,7 +153,10 @@ public enum MemberCategory { *

Contrary to other categories, this does not register any particular * reflection for inner classes but rather makes sure they are available * via a call to {@link Class#getClasses}. + * @deprecated with no replacement since introspection is included + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. */ + @Deprecated(since = "7.0", forRemoval = true) PUBLIC_CLASSES, /** @@ -127,7 +165,10 @@ public enum MemberCategory { *

Contrary to other categories, this does not register any particular * reflection for inner classes but rather makes sure they are available * via a call to {@link Class#getDeclaredClasses}. + * @deprecated with no replacement since introspection is included + * when {@link ReflectionHints#registerType(Class, MemberCategory...) adding a reflection hint for a type}. */ + @Deprecated(since = "7.0", forRemoval = true) DECLARED_CLASSES, /** diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java index c6fc5bc2a7c6..2a6ba90aaac0 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionHints.java @@ -26,8 +26,9 @@ import java.util.function.Consumer; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.TypeHint.Builder; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -59,8 +60,7 @@ public Stream typeHints() { * @param type the type to inspect * @return the reflection hints for this type, or {@code null} */ - @Nullable - public TypeHint getTypeHint(TypeReference type) { + public @Nullable TypeHint getTypeHint(TypeReference type) { Builder typeHintBuilder = this.types.get(type); return (typeHintBuilder != null ? typeHintBuilder.build() : null); } @@ -70,8 +70,7 @@ public TypeHint getTypeHint(TypeReference type) { * @param type the type to inspect * @return the reflection hints for this type, or {@code null} */ - @Nullable - public TypeHint getTypeHint(Class type) { + public @Nullable TypeHint getTypeHint(Class type) { return getTypeHint(TypeReference.of(type)); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java index 3e4a26557a9e..1e55f338b8a3 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ReflectionTypeReference.java @@ -16,7 +16,8 @@ package org.springframework.aot.hint; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -35,8 +36,7 @@ private ReflectionTypeReference(Class type) { this.type = type; } - @Nullable - private static TypeReference getEnclosingClass(Class type) { + private static @Nullable TypeReference getEnclosingClass(Class type) { Class candidate = (type.isArray() ? type.componentType().getEnclosingClass() : type.getEnclosingClass()); return (candidate != null ? new ReflectionTypeReference(candidate) : null); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourceBundleHint.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourceBundleHint.java index c6a652260d21..e14c9f3cc9ef 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourceBundleHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourceBundleHint.java @@ -19,7 +19,7 @@ import java.util.Objects; import java.util.ResourceBundle; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A hint that describes the need to access a {@link ResourceBundle}. @@ -32,8 +32,7 @@ public final class ResourceBundleHint implements ConditionalHint { private final String baseName; - @Nullable - private final TypeReference reachableType; + private final @Nullable TypeReference reachableType; ResourceBundleHint(Builder builder) { @@ -49,9 +48,8 @@ public String getBaseName() { return this.baseName; } - @Nullable @Override - public TypeReference getReachableType() { + public @Nullable TypeReference getReachableType() { return this.reachableType; } @@ -74,8 +72,7 @@ public static class Builder { private String baseName; - @Nullable - private TypeReference reachableType; + private @Nullable TypeReference reachableType; Builder(String baseName) { this.baseName = baseName; diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java index ebe6ab916957..c3eff2abe19c 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourceHints.java @@ -24,9 +24,10 @@ import java.util.function.Consumer; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHint.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHint.java index e686a86a3a1b..5dda53db96ae 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHint.java @@ -16,12 +16,11 @@ package org.springframework.aot.hint; -import java.util.Arrays; import java.util.Objects; -import java.util.regex.Pattern; -import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; /** @@ -31,16 +30,18 @@ * resource on the classpath, or alternatively may contain the special * {@code *} character to indicate a wildcard match. For example: *

    - *
  • {@code file.properties}: matches just the {@code file.properties} + *
  • "file.properties": matches just the {@code file.properties} * file at the root of the classpath.
  • - *
  • {@code com/example/file.properties}: matches just the + *
  • "com/example/file.properties": matches just the * {@code file.properties} file in {@code com/example/}.
  • - *
  • {@code *.properties}: matches all the files with a {@code .properties} - * extension anywhere in the classpath.
  • - *
  • {@code com/example/*.properties}: matches all the files with a {@code .properties} - * extension in {@code com/example/} and its child directories at any depth.
  • - *
  • {@code com/example/*}: matches all the files in {@code com/example/} + *
  • "*.properties": matches all the files with a {@code .properties} + * extension at the root of the classpath.
  • + *
  • "com/example/*.properties": matches all the files with a {@code .properties} + * extension in {@code com/example/}.
  • + *
  • "com/example/{@literal **}": matches all the files in {@code com/example/} * and its child directories at any depth.
  • + *
  • "com/example/{@literal **}/*.properties": matches all the files with a {@code .properties} + * extension in {@code com/example/} and its child directories at any depth.
  • *
* *

A resource pattern must not start with a slash ({@code /}) unless it is the @@ -54,10 +55,11 @@ */ public final class ResourcePatternHint implements ConditionalHint { + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); + private final String pattern; - @Nullable - private final TypeReference reachableType; + private final @Nullable TypeReference reachableType; ResourcePatternHint(String pattern, @Nullable TypeReference reachableType) { @@ -77,21 +79,15 @@ public String getPattern() { } /** - * Return the regex {@link Pattern} to use for identifying the resources to match. + * Whether the given path matches the current glob pattern. + * @param path the path to match against */ - public Pattern toRegex() { - String prefix = (this.pattern.startsWith("*") ? ".*" : ""); - String suffix = (this.pattern.endsWith("*") ? ".*" : ""); - String regex = Arrays.stream(this.pattern.split("\\*")) - .filter(s -> !s.isEmpty()) - .map(Pattern::quote) - .collect(Collectors.joining(".*", prefix, suffix)); - return Pattern.compile(regex); + public boolean matches(String path) { + return PATH_MATCHER.match(this.pattern, path); } - @Nullable @Override - public TypeReference getReachableType() { + public @Nullable TypeReference getReachableType() { return this.reachableType; } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java index 78c8ce9a6e55..d9b2c8bcd330 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/ResourcePatternHints.java @@ -22,7 +22,7 @@ import java.util.List; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A collection of {@link ResourcePatternHint} describing whether resources should @@ -38,12 +38,9 @@ public final class ResourcePatternHints { private final List includes; - private final List excludes; - private ResourcePatternHints(Builder builder) { this.includes = new ArrayList<>(builder.includes); - this.excludes = new ArrayList<>(builder.excludes); } /** @@ -54,14 +51,6 @@ public List getIncludes() { return this.includes; } - /** - * Return the exclude patterns to use to identify the resources to match. - * @return the exclude patterns - */ - public List getExcludes() { - return this.excludes; - } - /** * Builder for {@link ResourcePatternHints}. @@ -70,13 +59,11 @@ public static class Builder { private final Set includes = new LinkedHashSet<>(); - private final Set excludes = new LinkedHashSet<>(); - Builder() { } /** - * Include resources matching the specified patterns. + * Include resources matching the specified glob patterns. * @param reachableType the type that should be reachable for this hint to apply * @param includes the include patterns (see {@link ResourcePatternHint} documentation) * @return {@code this}, to facilitate method chaining @@ -129,7 +116,7 @@ private List expandToIncludeDirectories(String includePattern) { } /** - * Include resources matching the specified patterns. + * Include resources matching the specified glob patterns. * @param includes the include patterns (see {@link ResourcePatternHint} documentation) * @return {@code this}, to facilitate method chaining */ @@ -137,28 +124,6 @@ public Builder includes(String... includes) { return includes(null, includes); } - /** - * Exclude resources matching the specified patterns. - * @param reachableType the type that should be reachable for this hint to apply - * @param excludes the exclude patterns (see {@link ResourcePatternHint} documentation) - * @return {@code this}, to facilitate method chaining - */ - public Builder excludes(@Nullable TypeReference reachableType, String... excludes) { - List newExcludes = Arrays.stream(excludes) - .map(include -> new ResourcePatternHint(include, reachableType)).toList(); - this.excludes.addAll(newExcludes); - return this; - } - - /** - * Exclude resources matching the specified patterns. - * @param excludes the exclude patterns (see {@link ResourcePatternHint} documentation) - * @return {@code this}, to facilitate method chaining - */ - public Builder excludes(String... excludes) { - return excludes(null, excludes); - } - /** * Create {@link ResourcePatternHints} based on the state of this * builder. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java index 83db8b9fbff0..968f775b02d9 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/RuntimeHintsRegistrar.java @@ -16,7 +16,7 @@ package org.springframework.aot.hint; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Contract for registering {@link RuntimeHints} based on the {@link ClassLoader} diff --git a/spring-core/src/main/java/org/springframework/aot/hint/SerializationHints.java b/spring-core/src/main/java/org/springframework/aot/hint/SerializationHints.java index 70b9df428427..bca351b44918 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/SerializationHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/SerializationHints.java @@ -22,7 +22,7 @@ import java.util.function.Consumer; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Gather the need for Java serialization at runtime. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java index 62a3ff9978cf..98d7d03e3437 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/SimpleTypeReference.java @@ -20,7 +20,8 @@ import javax.lang.model.SourceVersion; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -34,8 +35,7 @@ final class SimpleTypeReference extends AbstractTypeReference { private static final List PRIMITIVE_NAMES = List.of("boolean", "byte", "short", "int", "long", "char", "float", "double", "void"); - @Nullable - private String canonicalName; + private @Nullable String canonicalName; SimpleTypeReference(String packageName, String simpleName, @Nullable TypeReference enclosingType) { super(packageName, simpleName, enclosingType); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java b/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java index 3418248c87cc..b23f8d83553d 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/TypeHint.java @@ -27,7 +27,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -42,8 +43,7 @@ public final class TypeHint implements ConditionalHint { private final TypeReference type; - @Nullable - private final TypeReference reachableType; + private final @Nullable TypeReference reachableType; private final Set fields; @@ -83,9 +83,8 @@ public TypeReference getType() { return this.type; } - @Nullable @Override - public TypeReference getReachableType() { + public @Nullable TypeReference getReachableType() { return this.reachableType; } @@ -144,8 +143,7 @@ public static class Builder { private final TypeReference type; - @Nullable - private TypeReference reachableType; + private @Nullable TypeReference reachableType; private final Set fields = new HashSet<>(); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java b/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java index 6bc2c8cf2886..809b2970dde2 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Type abstraction that can be used to refer to types that are not available as @@ -62,8 +62,7 @@ public interface TypeReference extends Comparable { * does not have an enclosing type. * @return the enclosing type, if any */ - @Nullable - TypeReference getEnclosingType(); + @Nullable TypeReference getEnclosingType(); /** * Create an instance based on the specified type. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessor.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessor.java index 129744a1c592..e3983f511213 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessor.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/RegisterReflectionReflectiveProcessor.java @@ -24,11 +24,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -80,8 +80,7 @@ protected void registerReflectionHints(ReflectionHints hints, Class target, M hints.registerType(target, type -> type.withMembers(memberCategories)); } - @Nullable - private Class loadClass(String className) { + private @Nullable Class loadClass(String className) { try { return ClassUtils.forName(className, getClass().getClassLoader()); } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java b/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java index fac70357524e..263ff633eb7e 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotation support for runtime hints. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.hint.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/aot/hint/package-info.java b/spring-core/src/main/java/org/springframework/aot/hint/package-info.java index 19bccf9a7fe3..d8448d481a6d 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/package-info.java @@ -2,9 +2,7 @@ * Support for registering the need for reflection, resources, java * serialization and proxies at runtime. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.hint; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java index 6a4a0be11538..406d46d5b39f 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicates.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,8 @@ import java.util.Set; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableHint; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; @@ -34,7 +36,6 @@ import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; import org.springframework.core.MethodIntrospector; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -80,24 +81,52 @@ public TypeHintPredicate onType(Class type) { *

The returned type exposes additional methods that refine the predicate behavior. * @param constructor the constructor * @return the {@link RuntimeHints} predicate + * @deprecated since 7.0 in favor of {@link #onConstructorInvocation(Constructor)} + * or {@link #onType(Class)}. */ + @Deprecated(since = "7.0", forRemoval = true) public ConstructorHintPredicate onConstructor(Constructor constructor) { Assert.notNull(constructor, "'constructor' must not be null"); return new ConstructorHintPredicate(constructor); } + /** + * Return a predicate that checks whether an invocation hint is registered for the given constructor. + * @param constructor the constructor + * @return the {@link RuntimeHints} predicate + * @since 7.0 + */ + public Predicate onConstructorInvocation(Constructor constructor) { + Assert.notNull(constructor, "'constructor' must not be null"); + return new ConstructorHintPredicate(constructor).invoke(); + } + /** * Return a predicate that checks whether a reflection hint is registered for the given method. * By default, both introspection and invocation hints match. *

The returned type exposes additional methods that refine the predicate behavior. * @param method the method * @return the {@link RuntimeHints} predicate + * @deprecated since 7.0 in favor of {@link #onMethodInvocation(Method)} + * or {@link #onType(Class)}. */ + @Deprecated(since = "7.0", forRemoval = true) public MethodHintPredicate onMethod(Method method) { Assert.notNull(method, "'method' must not be null"); return new MethodHintPredicate(method); } + /** + * Return a predicate that checks whether an invocation hint is registered for the given method. + * @param method the method + * @return the {@link RuntimeHints} predicate + * @since 7.0 + */ + public Predicate onMethodInvocation(Method method) { + Assert.notNull(method, "'method' must not be null"); + return new MethodHintPredicate(method).invoke(); + } + /** * Return a predicate that checks whether a reflection hint is registered for the method that matches the given selector. * This looks up a method on the given type with the expected name, if unique. @@ -107,13 +136,31 @@ public MethodHintPredicate onMethod(Method method) { * @param methodName the method name * @return the {@link RuntimeHints} predicate * @throws IllegalArgumentException if the method cannot be found or if multiple methods are found with the same name. + * @deprecated since 7.0 in favor of {@link #onMethodInvocation(Class, String)} + * or {@link #onType(Class)}. */ + @Deprecated(since = "7.0", forRemoval = true) public MethodHintPredicate onMethod(Class type, String methodName) { Assert.notNull(type, "'type' must not be null"); Assert.hasText(methodName, "'methodName' must not be empty"); return new MethodHintPredicate(getMethod(type, methodName)); } + /** + * Return a predicate that checks whether an invocation hint is registered for the method that matches the given selector. + * This looks up a method on the given type with the expected name, if unique. + * @param type the type holding the method + * @param methodName the method name + * @return the {@link RuntimeHints} predicate + * @throws IllegalArgumentException if the method cannot be found or if multiple methods are found with the same name. + * @since 7.0 + */ + public Predicate onMethodInvocation(Class type, String methodName) { + Assert.notNull(type, "'type' must not be null"); + Assert.hasText(methodName, "'methodName' must not be empty"); + return new MethodHintPredicate(getMethod(type, methodName)).invoke(); + } + /** * Return a predicate that checks whether a reflection hint is registered for the method that matches the given selector. * This looks up a method on the given type with the expected name, if unique. @@ -124,13 +171,32 @@ public MethodHintPredicate onMethod(Class type, String methodName) { * @return the {@link RuntimeHints} predicate * @throws ClassNotFoundException if the class cannot be resolved. * @throws IllegalArgumentException if the method cannot be found or if multiple methods are found with the same name. + * @deprecated since 7.0 in favor of {@link #onMethodInvocation(String, String)} + * or {@link #onType(Class)}. */ + @Deprecated(since = "7.0", forRemoval = true) public MethodHintPredicate onMethod(String className, String methodName) throws ClassNotFoundException { Assert.hasText(className, "'className' must not be empty"); Assert.hasText(methodName, "'methodName' must not be empty"); return onMethod(Class.forName(className), methodName); } + /** + * Return a predicate that checks whether an invocation hint is registered for the method that matches the given selector. + * This looks up a method on the given type with the expected name, if unique. + * @param className the name of the class holding the method + * @param methodName the method name + * @return the {@link RuntimeHints} predicate + * @throws ClassNotFoundException if the class cannot be resolved. + * @throws IllegalArgumentException if the method cannot be found or if multiple methods are found with the same name. + * @since 7.0 + */ + public Predicate onMethodInvocation(String className, String methodName) throws ClassNotFoundException { + Assert.hasText(className, "'className' must not be empty"); + Assert.hasText(methodName, "'methodName' must not be empty"); + return onMethod(Class.forName(className), methodName).invoke(); + } + private Method getMethod(Class type, String methodName) { ReflectionUtils.MethodFilter selector = method -> methodName.equals(method.getName()); Set methods = MethodIntrospector.selectMethods(type, selector); @@ -146,16 +212,29 @@ else if (methods.size() > 1) { } /** - * Return a predicate that checks whether a reflection hint is registered for the field that matches the given selector. + * Return a predicate that checks whether a reflective field access hint is registered for the field. + * This looks up a field on the given type with the expected name, if present. + * @param type the type holding the field + * @param fieldName the field name + * @return the {@link RuntimeHints} predicate + * @throws IllegalArgumentException if a field cannot be found with the given name. + * @deprecated since 7.0 in favor of {@link #onFieldAccess(Class, String)} with similar semantics. + */ + @Deprecated(since = "7.0", forRemoval = true) + public Predicate onField(Class type, String fieldName) { + return onFieldAccess(type, fieldName); + } + + /** + * Return a predicate that checks whether a reflective field access hint is registered for the field. * This looks up a field on the given type with the expected name, if present. - * By default, unsafe or write access is not considered. - *

The returned type exposes additional methods that refine the predicate behavior. * @param type the type holding the field * @param fieldName the field name * @return the {@link RuntimeHints} predicate * @throws IllegalArgumentException if a field cannot be found with the given name. + * @since 7.0 */ - public FieldHintPredicate onField(Class type, String fieldName) { + public Predicate onFieldAccess(Class type, String fieldName) { Assert.notNull(type, "'type' must not be null"); Assert.hasText(fieldName, "'fieldName' must not be empty"); Field field = ReflectionUtils.findField(type, fieldName); @@ -166,28 +245,54 @@ public FieldHintPredicate onField(Class type, String fieldName) { } /** - * Return a predicate that checks whether a reflection hint is registered for the field that matches the given selector. + * Return a predicate that checks whether a reflective field access hint is registered for the field. * This looks up a field on the given type with the expected name, if present. - * By default, unsafe or write access is not considered. - *

The returned type exposes additional methods that refine the predicate behavior. * @param className the name of the class holding the field * @param fieldName the field name * @return the {@link RuntimeHints} predicate * @throws ClassNotFoundException if the class cannot be resolved. * @throws IllegalArgumentException if a field cannot be found with the given name. + * @deprecated since 7.0 in favor of {@link #onFieldAccess(String, String)} with similar semantics. */ - public FieldHintPredicate onField(String className, String fieldName) throws ClassNotFoundException { + @Deprecated(since = "7.0", forRemoval = true) + public Predicate onField(String className, String fieldName) throws ClassNotFoundException { + return onFieldAccess(className, fieldName); + } + + /** + * Return a predicate that checks whether an invocation hint is registered for the field. + * This looks up a field on the given type with the expected name, if present. + * @param className the name of the class holding the field + * @param fieldName the field name + * @return the {@link RuntimeHints} predicate + * @throws ClassNotFoundException if the class cannot be resolved. + * @throws IllegalArgumentException if a field cannot be found with the given name. + * @since 7.0 + */ + public Predicate onFieldAccess(String className, String fieldName) throws ClassNotFoundException { Assert.hasText(className, "'className' must not be empty"); Assert.hasText(fieldName, "'fieldName' must not be empty"); - return onField(Class.forName(className), fieldName); + return onFieldAccess(Class.forName(className), fieldName); } /** * Return a predicate that checks whether a reflective field access hint is registered for the given field. * @param field the field * @return the {@link RuntimeHints} predicate + * @deprecated since 7.0 in favor of {@link #onFieldAccess(Field)} with similar semantics. */ - public FieldHintPredicate onField(Field field) { + @Deprecated(since = "7.0", forRemoval = true) + public Predicate onField(Field field) { + return onFieldAccess(field); + } + + /** + * Return a predicate that checks whether an invocation hint is registered for the given field. + * @param field the field + * @return the {@link RuntimeHints} predicate + * @since 7.0 + */ + public Predicate onFieldAccess(Field field) { Assert.notNull(field, "'field' must not be null"); return new FieldHintPredicate(field); } @@ -201,8 +306,7 @@ public static class TypeHintPredicate implements Predicate { this.type = type; } - @Nullable - private TypeHint getTypeHint(RuntimeHints hints) { + private @Nullable TypeHint getTypeHint(RuntimeHints hints) { return hints.reflection().getTypeHint(this.type); } @@ -252,7 +356,8 @@ public Predicate withAnyMemberCategory(MemberCategory... memberCat } } - + @Deprecated(since = "7.0", forRemoval = true) + @SuppressWarnings("removal") public abstract static class ExecutableHintPredicate implements Predicate { protected final T executable; @@ -297,6 +402,8 @@ static boolean includes(ExecutableHint hint, String name, } + @Deprecated(since = "7.0", forRemoval = true) + @SuppressWarnings("removal") public static class ConstructorHintPredicate extends ExecutableHintPredicate> { ConstructorHintPredicate(Constructor constructor) { @@ -306,28 +413,17 @@ public static class ConstructorHintPredicate extends ExecutableHintPredicate Modifier.isPublic(this.executable.getModifiers()))) - .or(new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())).withAnyMemberCategory(getDeclaredMemberCategories())) + .and(hints -> this.executableMode == ExecutableMode.INTROSPECT)) + .or(new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())) + .withMemberCategory(MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS) + .and(hints -> Modifier.isPublic(this.executable.getModifiers())) + .and(hints -> this.executableMode == ExecutableMode.INVOKE)) + .or(new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS) + .and(hints -> this.executableMode == ExecutableMode.INVOKE)) .or(exactMatch()).test(runtimeHints); } - MemberCategory[] getPublicMemberCategories() { - if (this.executableMode == ExecutableMode.INTROSPECT) { - return new MemberCategory[] { MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS }; - } - return new MemberCategory[] { MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS }; - } - - MemberCategory[] getDeclaredMemberCategories() { - if (this.executableMode == ExecutableMode.INTROSPECT) { - return new MemberCategory[] { MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_DECLARED_CONSTRUCTORS }; - } - return new MemberCategory[] { MemberCategory.INVOKE_DECLARED_CONSTRUCTORS }; - } - @Override Predicate exactMatch() { return hints -> { @@ -341,6 +437,8 @@ Predicate exactMatch() { } + @Deprecated(since = "7.0", forRemoval = true) + @SuppressWarnings("removal") public static class MethodHintPredicate extends ExecutableHintPredicate { MethodHintPredicate(Method method) { @@ -350,31 +448,18 @@ public static class MethodHintPredicate extends ExecutableHintPredicate @Override public boolean test(RuntimeHints runtimeHints) { return (new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())) - .withAnyMemberCategory(getPublicMemberCategories()) - .and(hints -> Modifier.isPublic(this.executable.getModifiers()))) - .or(new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())) - .withAnyMemberCategory(getDeclaredMemberCategories()) - .and(hints -> !Modifier.isPublic(this.executable.getModifiers()))) + .and(hints -> this.executableMode == ExecutableMode.INTROSPECT)) + .or((new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())) + .withMemberCategory(MemberCategory.INVOKE_PUBLIC_METHODS) + .and(hints -> Modifier.isPublic(this.executable.getModifiers())) + .and(hints -> this.executableMode == ExecutableMode.INVOKE))) + .or((new TypeHintPredicate(TypeReference.of(this.executable.getDeclaringClass())) + .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS) + .and(hints -> !Modifier.isPublic(this.executable.getModifiers())) + .and(hints -> this.executableMode == ExecutableMode.INVOKE))) .or(exactMatch()).test(runtimeHints); } - MemberCategory[] getPublicMemberCategories() { - if (this.executableMode == ExecutableMode.INTROSPECT) { - return new MemberCategory[] { MemberCategory.INTROSPECT_PUBLIC_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS }; - } - return new MemberCategory[] { MemberCategory.INVOKE_PUBLIC_METHODS }; - } - - MemberCategory[] getDeclaredMemberCategories() { - - if (this.executableMode == ExecutableMode.INTROSPECT) { - return new MemberCategory[] { MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_DECLARED_METHODS }; - } - return new MemberCategory[] { MemberCategory.INVOKE_DECLARED_METHODS }; - } - @Override Predicate exactMatch() { return hints -> { @@ -388,6 +473,7 @@ Predicate exactMatch() { } + @Deprecated(since = "7.0", forRemoval = true) public static class FieldHintPredicate implements Predicate { private final Field field; @@ -405,12 +491,15 @@ public boolean test(RuntimeHints runtimeHints) { return memberCategoryMatch(typeHint) || exactMatch(typeHint); } + @SuppressWarnings("removal") private boolean memberCategoryMatch(TypeHint typeHint) { if (Modifier.isPublic(this.field.getModifiers())) { - return typeHint.getMemberCategories().contains(MemberCategory.PUBLIC_FIELDS); + return typeHint.getMemberCategories().contains(MemberCategory.ACCESS_PUBLIC_FIELDS) || + typeHint.getMemberCategories().contains(MemberCategory.PUBLIC_FIELDS); } else { - return typeHint.getMemberCategories().contains(MemberCategory.DECLARED_FIELDS); + return typeHint.getMemberCategories().contains(MemberCategory.ACCESS_DECLARED_FIELDS) || + typeHint.getMemberCategories().contains(MemberCategory.DECLARED_FIELDS); } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ResourceHintsPredicates.java b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ResourceHintsPredicates.java index d59d48af6cf0..302354694360 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/predicate/ResourceHintsPredicates.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/predicate/ResourceHintsPredicates.java @@ -19,14 +19,13 @@ import java.util.ArrayList; import java.util.List; import java.util.function.Predicate; -import java.util.regex.Pattern; import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.ResourcePatternHint; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeReference; +import org.springframework.util.AntPathMatcher; import org.springframework.util.Assert; -import org.springframework.util.ConcurrentLruCache; /** * Generator of {@link ResourceHints} predicates, testing whether the given hints @@ -39,7 +38,7 @@ */ public class ResourceHintsPredicates { - private static final ConcurrentLruCache CACHED_RESOURCE_PATTERNS = new ConcurrentLruCache<>(32, ResourcePatternHint::toRegex); + private static final AntPathMatcher PATH_MATCHER = new AntPathMatcher(); ResourceHintsPredicates() { } @@ -100,26 +99,18 @@ public Predicate forResource(String resourceName) { return hints -> { AggregatedResourcePatternHints aggregatedResourcePatternHints = AggregatedResourcePatternHints.of( hints.resources()); - boolean isExcluded = aggregatedResourcePatternHints.excludes().stream().anyMatch(excluded -> - CACHED_RESOURCE_PATTERNS.get(excluded).matcher(resourceNameToUse).matches()); - if (isExcluded) { - return false; - } return aggregatedResourcePatternHints.includes().stream().anyMatch(included -> - CACHED_RESOURCE_PATTERNS.get(included).matcher(resourceNameToUse).matches()); + PATH_MATCHER.match(included.getPattern(), resourceNameToUse)); }; } - private record AggregatedResourcePatternHints(List includes, List excludes) { + private record AggregatedResourcePatternHints(List includes) { static AggregatedResourcePatternHints of(ResourceHints resourceHints) { List includes = new ArrayList<>(); - List excludes = new ArrayList<>(); - resourceHints.resourcePatternHints().forEach(resourcePatternHint -> { - includes.addAll(resourcePatternHint.getIncludes()); - excludes.addAll(resourcePatternHint.getExcludes()); - }); - return new AggregatedResourcePatternHints(includes, excludes); + resourceHints.resourcePatternHints().forEach(resourcePatternHint -> + includes.addAll(resourcePatternHint.getIncludes())); + return new AggregatedResourcePatternHints(includes); } } diff --git a/spring-core/src/main/java/org/springframework/aot/hint/predicate/package-info.java b/spring-core/src/main/java/org/springframework/aot/hint/predicate/package-info.java index 8d6d410e07a2..5bb1f8bf6cb0 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/predicate/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/predicate/package-info.java @@ -1,9 +1,7 @@ /** * Predicate support for runtime hints. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.hint.predicate; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/ClassHintUtils.java b/spring-core/src/main/java/org/springframework/aot/hint/support/ClassHintUtils.java index ffe6c247c87e..50d16ac2e3a4 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/ClassHintUtils.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/ClassHintUtils.java @@ -44,7 +44,7 @@ public abstract class ClassHintUtils { private static final Consumer asClassBasedProxy = hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.DECLARED_FIELDS); + MemberCategory.ACCESS_DECLARED_FIELDS); private static final Consumer asProxiedUserClass = hint -> hint.withMembers(MemberCategory.INVOKE_PUBLIC_METHODS, diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java b/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java index a41d5d31f28c..c62fdf59f79e 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrar.java @@ -20,8 +20,9 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ResourceHints; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ResourceUtils; @@ -36,9 +37,10 @@ * * @author Stephane Nicoll * @author Sam Brannen + * @author Juergen Hoeller * @since 6.0 */ -public class FilePatternResourceHintsRegistrar { +public final class FilePatternResourceHintsRegistrar { private final List classpathLocations; @@ -47,26 +49,16 @@ public class FilePatternResourceHintsRegistrar { private final List fileExtensions; - /** - * Create a new instance for the specified file prefixes, classpath locations, - * and file extensions. - * @param filePrefixes the file prefixes - * @param classpathLocations the classpath locations - * @param fileExtensions the file extensions (starting with a dot) - * @deprecated as of 6.0.12 in favor of {@linkplain #forClassPathLocations(String...) the builder} - */ - @Deprecated(since = "6.0.12", forRemoval = true) - public FilePatternResourceHintsRegistrar(List filePrefixes, List classpathLocations, + private FilePatternResourceHintsRegistrar(List filePrefixes, List classpathLocations, List fileExtensions) { - this.classpathLocations = validateClasspathLocations(classpathLocations); + this.classpathLocations = validateClassPathLocations(classpathLocations); this.filePrefixes = validateFilePrefixes(filePrefixes); this.fileExtensions = validateFileExtensions(fileExtensions); } - @Deprecated(since = "6.0.12", forRemoval = true) - public void registerHints(ResourceHints hints, @Nullable ClassLoader classLoader) { + private void registerHints(ResourceHints hints, @Nullable ClassLoader classLoader) { ClassLoader classLoaderToUse = (classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader()); if (classLoaderToUse != null) { List includes = new ArrayList<>(); @@ -88,7 +80,7 @@ public void registerHints(ResourceHints hints, @Nullable ClassLoader classLoader /** * Configure the registrar with the specified - * {@linkplain Builder#withClasspathLocations(String...) classpath locations}. + * {@linkplain Builder#withClassPathLocations(String...) classpath locations}. * @param classpathLocations the classpath locations * @return a {@link Builder} to further configure the registrar * @since 6.0.12 @@ -100,17 +92,17 @@ public static Builder forClassPathLocations(String... classpathLocations) { /** * Configure the registrar with the specified - * {@linkplain Builder#withClasspathLocations(List) classpath locations}. + * {@linkplain Builder#withClassPathLocations(List) classpath locations}. * @param classpathLocations the classpath locations * @return a {@link Builder} to further configure the registrar * @since 6.0.12 * @see #forClassPathLocations(String...) */ public static Builder forClassPathLocations(List classpathLocations) { - return new Builder().withClasspathLocations(classpathLocations); + return new Builder().withClassPathLocations(classpathLocations); } - private static List validateClasspathLocations(List classpathLocations) { + private static List validateClassPathLocations(List classpathLocations) { Assert.notEmpty(classpathLocations, "At least one classpath location must be specified"); List parsedLocations = new ArrayList<>(); for (String location : classpathLocations) { @@ -163,6 +155,24 @@ private Builder() { // no-op } + /** + * Consider the specified classpath locations. + * @deprecated in favor of {@link #withClassPathLocations(String...)} + */ + @Deprecated(since = "7.0", forRemoval = true) + public Builder withClasspathLocations(String... classpathLocations) { + return withClassPathLocations(Arrays.asList(classpathLocations)); + } + + /** + * Consider the specified classpath locations. + * @deprecated in favor of {@link #withClassPathLocations(List)} + */ + @Deprecated(since = "7.0", forRemoval = true) + public Builder withClasspathLocations(List classpathLocations) { + return withClassPathLocations(classpathLocations); + } + /** * Consider the specified classpath locations. *

A location can either be a special {@value ResourceUtils#CLASSPATH_URL_PREFIX} @@ -170,10 +180,11 @@ private Builder() { * An empty String represents the root of the classpath. * @param classpathLocations the classpath locations to consider * @return this builder - * @see #withClasspathLocations(List) + * @since 7.0 + * @see #withClassPathLocations(List) */ - public Builder withClasspathLocations(String... classpathLocations) { - return withClasspathLocations(Arrays.asList(classpathLocations)); + public Builder withClassPathLocations(String... classpathLocations) { + return withClassPathLocations(Arrays.asList(classpathLocations)); } /** @@ -183,10 +194,11 @@ public Builder withClasspathLocations(String... classpathLocations) { * An empty String represents the root of the classpath. * @param classpathLocations the classpath locations to consider * @return this builder - * @see #withClasspathLocations(String...) + * @since 7.0 + * @see #withClassPathLocations(String...) */ - public Builder withClasspathLocations(List classpathLocations) { - this.classpathLocations.addAll(validateClasspathLocations(classpathLocations)); + public Builder withClassPathLocations(List classpathLocations) { + this.classpathLocations.addAll(validateClassPathLocations(classpathLocations)); return this; } @@ -238,7 +250,6 @@ public Builder withFileExtensions(List fileExtensions) { return this; } - private FilePatternResourceHintsRegistrar build() { return new FilePatternResourceHintsRegistrar(this.filePrefixes, this.classpathLocations, this.fileExtensions); diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java index ed820c4a1532..2f3137155a83 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/KotlinDetectorRuntimeHints.java @@ -16,10 +16,11 @@ package org.springframework.aot.hint.support; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} to register hints for {@link org.springframework.core.KotlinDetector}. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java index c086c2c7ec76..b477cbce80fc 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHints.java @@ -20,13 +20,14 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} to register hints for popular conventions in diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java index 371812e06c5b..60f1203c7caf 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/PathMatchingResourcePatternResolverRuntimeHints.java @@ -16,11 +16,12 @@ package org.springframework.aot.hint.support; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} for {@link PathMatchingResourcePatternResolver}. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java index 819361ffff1b..5f98f4a4ea59 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringFactoriesLoaderRuntimeHints.java @@ -21,13 +21,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -91,8 +91,7 @@ private void registerHints(RuntimeHints hints, ClassLoader classLoader, } } - @Nullable - private Class resolveClassName(ClassLoader classLoader, String factoryClassName) { + private @Nullable Class resolveClassName(ClassLoader classLoader, String factoryClassName) { try { Class clazz = ClassUtils.resolveClassName(factoryClassName, classLoader); // Force resolution of all constructors to cache diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java index 50e61caa4dae..e6cd63d4767d 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/SpringPropertiesRuntimeHints.java @@ -16,9 +16,10 @@ package org.springframework.aot.hint.support; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} to register hints for {@link org.springframework.core.SpringProperties}. diff --git a/spring-core/src/main/java/org/springframework/aot/hint/support/package-info.java b/spring-core/src/main/java/org/springframework/aot/hint/support/package-info.java index 87ce610cfe09..02331601b1ee 100644 --- a/spring-core/src/main/java/org/springframework/aot/hint/support/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/hint/support/package-info.java @@ -1,9 +1,7 @@ /** * Convenience classes for using runtime hints. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.hint.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationWriter.java index 22a0cd5c9d1f..2ddc0dc7a373 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/FileNativeConfigurationWriter.java @@ -23,7 +23,7 @@ import java.nio.file.Path; import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@link NativeConfigurationWriter} implementation that writes the @@ -38,11 +38,9 @@ public class FileNativeConfigurationWriter extends NativeConfigurationWriter { private final Path basePath; - @Nullable - private final String groupId; + private final @Nullable String groupId; - @Nullable - private final String artifactId; + private final @Nullable String artifactId; public FileNativeConfigurationWriter(Path basePath) { this(basePath, null, null); diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java index 5ef7d21e2656..56d51b30efc7 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/NativeConfigurationWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,11 +18,7 @@ import java.util.function.Consumer; -import org.springframework.aot.hint.ProxyHints; -import org.springframework.aot.hint.ReflectionHints; -import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.RuntimeHints; -import org.springframework.aot.hint.SerializationHints; /** * Write {@link RuntimeHints} as GraalVM native configuration. @@ -30,8 +26,9 @@ * @author Sebastien Deleuze * @author Stephane Nicoll * @author Janne Valkealahti + * @author Brian Clozel * @since 6.0 - * @see Native Image Build Configuration + * @see Native Image Build Configuration */ public abstract class NativeConfigurationWriter { @@ -40,24 +37,21 @@ public abstract class NativeConfigurationWriter { * @param hints the hints to handle */ public void write(RuntimeHints hints) { - if (hints.serialization().javaSerializationHints().findAny().isPresent()) { - writeSerializationHints(hints.serialization()); - } - if (hints.proxies().jdkProxyHints().findAny().isPresent()) { - writeProxyHints(hints.proxies()); - } - if (hints.reflection().typeHints().findAny().isPresent()) { - writeReflectionHints(hints.reflection()); - } - if (hints.resources().resourcePatternHints().findAny().isPresent() || - hints.resources().resourceBundleHints().findAny().isPresent()) { - writeResourceHints(hints.resources()); - } - if (hints.jni().typeHints().findAny().isPresent()) { - writeJniHints(hints.jni()); + if (hasAnyHint(hints)) { + writeTo("reachability-metadata.json", + writer -> new RuntimeHintsWriter().write(writer, hints)); } } + private boolean hasAnyHint(RuntimeHints hints) { + return (hints.serialization().javaSerializationHints().findAny().isPresent() || + hints.proxies().jdkProxyHints().findAny().isPresent() || + hints.reflection().typeHints().findAny().isPresent() || + hints.resources().resourcePatternHints().findAny().isPresent() || + hints.resources().resourceBundleHints().findAny().isPresent() || + hints.jni().typeHints().findAny().isPresent()); + } + /** * Write the specified GraalVM native configuration file, using the * provided {@link BasicJsonWriter}. @@ -66,29 +60,4 @@ public void write(RuntimeHints hints) { */ protected abstract void writeTo(String fileName, Consumer writer); - private void writeSerializationHints(SerializationHints hints) { - writeTo("serialization-config.json", writer -> - SerializationHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeProxyHints(ProxyHints hints) { - writeTo("proxy-config.json", writer -> - ProxyHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeReflectionHints(ReflectionHints hints) { - writeTo("reflect-config.json", writer -> - ReflectionHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeResourceHints(ResourceHints hints) { - writeTo("resource-config.json", writer -> - ResourceHintsWriter.INSTANCE.write(writer, hints)); - } - - private void writeJniHints(ReflectionHints hints) { - writeTo("jni-config.json", writer -> - ReflectionHintsWriter.INSTANCE.write(writer, hints)); - } - } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java deleted file mode 100644 index 51cd3132efac..000000000000 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.aot.nativex; - -import java.util.Comparator; -import java.util.LinkedHashMap; -import java.util.Map; -import java.util.stream.Collectors; - -import org.springframework.aot.hint.JdkProxyHint; -import org.springframework.aot.hint.ProxyHints; -import org.springframework.aot.hint.TypeReference; - -/** - * Write {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON - * output expected by the GraalVM {@code native-image} compiler, typically named - * {@code proxy-config.json}. - * - * @author Sebastien Deleuze - * @author Stephane Nicoll - * @author Brian Clozel - * @since 6.0 - * @see Dynamic Proxy in Native Image - * @see Native Image Build Configuration - */ -class ProxyHintsWriter { - - public static final ProxyHintsWriter INSTANCE = new ProxyHintsWriter(); - - private static final Comparator JDK_PROXY_HINT_COMPARATOR = - (left, right) -> { - String leftSignature = left.getProxiedInterfaces().stream() - .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); - String rightSignature = right.getProxiedInterfaces().stream() - .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); - return leftSignature.compareTo(rightSignature); - }; - - public void write(BasicJsonWriter writer, ProxyHints hints) { - writer.writeArray(hints.jdkProxyHints().sorted(JDK_PROXY_HINT_COMPARATOR) - .map(this::toAttributes).toList()); - } - - private Map toAttributes(JdkProxyHint hint) { - Map attributes = new LinkedHashMap<>(); - handleCondition(attributes, hint); - attributes.put("interfaces", hint.getProxiedInterfaces()); - return attributes; - } - - private void handleCondition(Map attributes, JdkProxyHint hint) { - if (hint.getReachableType() != null) { - Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); - attributes.put("condition", conditionAttributes); - } - } - -} diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java similarity index 58% rename from spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java index df360b73b137..54ed6dbb4e39 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,48 +16,73 @@ package org.springframework.aot.nativex; +import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + +import org.springframework.aot.hint.ConditionalHint; import org.springframework.aot.hint.ExecutableHint; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.FieldHint; +import org.springframework.aot.hint.JdkProxyHint; import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.TypeHint; -import org.springframework.lang.Nullable; +import org.springframework.aot.hint.TypeReference; /** - * Write {@link ReflectionHints} to the JSON output expected by the GraalVM - * {@code native-image} compiler, typically named {@code reflect-config.json} - * or {@code jni-config.json}. + * Collect {@link ReflectionHints} as map attributes ready for JSON serialization for the GraalVM + * {@code native-image} compiler. * * @author Sebastien Deleuze * @author Stephane Nicoll * @author Janne Valkealahti - * @since 6.0 - * @see Reflection Use in Native Images - * @see Java Native Interface (JNI) in Native Image - * @see Native Image Build Configuration + * @see Reflection Use in Native Images + * @see Java Native Interface (JNI) in Native Image + * @see Native Image Build Configuration */ -class ReflectionHintsWriter { +class ReflectionHintsAttributes { - public static final ReflectionHintsWriter INSTANCE = new ReflectionHintsWriter(); + private static final Comparator JDK_PROXY_HINT_COMPARATOR = + (left, right) -> { + String leftSignature = left.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + String rightSignature = right.getProxiedInterfaces().stream() + .map(TypeReference::getCanonicalName).collect(Collectors.joining(",")); + return leftSignature.compareTo(rightSignature); + }; - public void write(BasicJsonWriter writer, ReflectionHints hints) { - writer.writeArray(hints.typeHints() + public List> reflection(RuntimeHints hints) { + List> reflectionHints = new ArrayList<>(); + reflectionHints.addAll(hints.reflection().typeHints() .sorted(Comparator.comparing(TypeHint::getType)) .map(this::toAttributes).toList()); + reflectionHints.addAll(hints.proxies().jdkProxyHints() + .sorted(JDK_PROXY_HINT_COMPARATOR) + .map(this::toAttributes).toList()); + return reflectionHints; + } + + public List> jni(RuntimeHints hints) { + List> jniHints = new ArrayList<>(); + jniHints.addAll(hints.jni().typeHints() + .sorted(Comparator.comparing(TypeHint::getType)) + .map(this::toAttributes).toList()); + return jniHints; } private Map toAttributes(TypeHint hint) { Map attributes = new LinkedHashMap<>(); - attributes.put("name", hint.getType()); + attributes.put("type", hint.getType()); handleCondition(attributes, hint); handleCategories(attributes, hint.getMemberCategories()); handleFields(attributes, hint.fields()); @@ -66,33 +91,23 @@ private Map toAttributes(TypeHint hint) { return attributes; } - private void handleCondition(Map attributes, TypeHint hint) { + private void handleCondition(Map attributes, ConditionalHint hint) { if (hint.getReachableType() != null) { - Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); - attributes.put("condition", conditionAttributes); + attributes.put("condition", Map.of("typeReached", hint.getReachableType())); } } private void handleFields(Map attributes, Stream fields) { addIfNotEmpty(attributes, "fields", fields .sorted(Comparator.comparing(FieldHint::getName, String::compareToIgnoreCase)) - .map(this::toAttributes).toList()); - } - - private Map toAttributes(FieldHint hint) { - Map attributes = new LinkedHashMap<>(); - attributes.put("name", hint.getName()); - return attributes; + .map(fieldHint -> Map.of("name", fieldHint.getName())) + .toList()); } private void handleExecutables(Map attributes, List hints) { addIfNotEmpty(attributes, "methods", hints.stream() .filter(h -> h.getMode().equals(ExecutableMode.INVOKE)) .map(this::toAttributes).toList()); - addIfNotEmpty(attributes, "queriedMethods", hints.stream() - .filter(h -> h.getMode().equals(ExecutableMode.INTROSPECT)) - .map(this::toAttributes).toList()); } private Map toAttributes(ExecutableHint hint) { @@ -102,23 +117,16 @@ private Map toAttributes(ExecutableHint hint) { return attributes; } + @SuppressWarnings("removal") private void handleCategories(Map attributes, Set categories) { categories.stream().sorted().forEach(category -> { switch (category) { - case PUBLIC_FIELDS -> attributes.put("allPublicFields", true); - case DECLARED_FIELDS -> attributes.put("allDeclaredFields", true); - case INTROSPECT_PUBLIC_CONSTRUCTORS -> - attributes.put("queryAllPublicConstructors", true); - case INTROSPECT_DECLARED_CONSTRUCTORS -> - attributes.put("queryAllDeclaredConstructors", true); + case ACCESS_PUBLIC_FIELDS, PUBLIC_FIELDS -> attributes.put("allPublicFields", true); + case ACCESS_DECLARED_FIELDS, DECLARED_FIELDS -> attributes.put("allDeclaredFields", true); case INVOKE_PUBLIC_CONSTRUCTORS -> attributes.put("allPublicConstructors", true); case INVOKE_DECLARED_CONSTRUCTORS -> attributes.put("allDeclaredConstructors", true); - case INTROSPECT_PUBLIC_METHODS -> - attributes.put("queryAllPublicMethods", true); - case INTROSPECT_DECLARED_METHODS -> - attributes.put("queryAllDeclaredMethods", true); case INVOKE_PUBLIC_METHODS -> attributes.put("allPublicMethods", true); case INVOKE_DECLARED_METHODS -> attributes.put("allDeclaredMethods", true); @@ -136,4 +144,11 @@ private void addIfNotEmpty(Map attributes, String name, @Nullabl } } + private Map toAttributes(JdkProxyHint hint) { + Map attributes = new LinkedHashMap<>(); + handleCondition(attributes, hint); + attributes.put("type", Map.of("proxy", hint.getProxiedInterfaces())); + return attributes; + } + } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsAttributes.java similarity index 56% rename from spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java rename to spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsAttributes.java index 6829006e9029..36cf54894959 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,23 +16,20 @@ package org.springframework.aot.nativex; -import java.util.Collection; import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.stream.Stream; import org.springframework.aot.hint.ConditionalHint; import org.springframework.aot.hint.ResourceBundleHint; import org.springframework.aot.hint.ResourceHints; import org.springframework.aot.hint.ResourcePatternHint; import org.springframework.aot.hint.ResourcePatternHints; -import org.springframework.lang.Nullable; /** - * Write a {@link ResourceHints} to the JSON output expected by the GraalVM - * {@code native-image} compiler, typically named {@code resource-config.json}. + * Collect {@link ResourceHints} as map attributes ready for JSON serialization for the GraalVM + * {@code native-image} compiler. * * @author Sebastien Deleuze * @author Stephane Nicoll @@ -41,9 +38,7 @@ * @see Accessing Resources in Native Images * @see Native Image Build Configuration */ -class ResourceHintsWriter { - - public static final ResourceHintsWriter INSTANCE = new ResourceHintsWriter(); +class ResourceHintsAttributes { private static final Comparator RESOURCE_PATTERN_HINT_COMPARATOR = Comparator.comparing(ResourcePatternHint::getPattern); @@ -52,30 +47,17 @@ class ResourceHintsWriter { Comparator.comparing(ResourceBundleHint::getBaseName); - public void write(BasicJsonWriter writer, ResourceHints hints) { - Map attributes = new LinkedHashMap<>(); - addIfNotEmpty(attributes, "resources", toAttributes(hints)); - handleResourceBundles(attributes, hints.resourceBundleHints()); - writer.writeObject(attributes); - } - - private Map toAttributes(ResourceHints hint) { - Map attributes = new LinkedHashMap<>(); - addIfNotEmpty(attributes, "includes", hint.resourcePatternHints() + public List> resources(ResourceHints hint) { + return hint.resourcePatternHints() .map(ResourcePatternHints::getIncludes).flatMap(List::stream).distinct() .sorted(RESOURCE_PATTERN_HINT_COMPARATOR) - .map(this::toAttributes).toList()); - addIfNotEmpty(attributes, "excludes", hint.resourcePatternHints() - .map(ResourcePatternHints::getExcludes).flatMap(List::stream).distinct() - .sorted(RESOURCE_PATTERN_HINT_COMPARATOR) - .map(this::toAttributes).toList()); - return attributes; + .map(this::toAttributes).toList(); } - private void handleResourceBundles(Map attributes, Stream resourceBundles) { - addIfNotEmpty(attributes, "bundles", resourceBundles + public List> resourceBundles(ResourceHints hint) { + return hint.resourceBundleHints() .sorted(RESOURCE_BUNDLE_HINT_COMPARATOR) - .map(this::toAttributes).toList()); + .map(this::toAttributes).toList(); } private Map toAttributes(ResourceBundleHint hint) { @@ -88,30 +70,14 @@ private Map toAttributes(ResourceBundleHint hint) { private Map toAttributes(ResourcePatternHint hint) { Map attributes = new LinkedHashMap<>(); handleCondition(attributes, hint); - attributes.put("pattern", hint.toRegex().toString()); + attributes.put("glob", hint.getPattern()); return attributes; } - private void addIfNotEmpty(Map attributes, String name, @Nullable Object value) { - if (value instanceof Collection collection) { - if (!collection.isEmpty()) { - attributes.put(name, value); - } - } - else if (value instanceof Map map) { - if (!map.isEmpty()) { - attributes.put(name, value); - } - } - else if (value != null) { - attributes.put(name, value); - } - } - private void handleCondition(Map attributes, ConditionalHint hint) { if (hint.getReachableType() != null) { Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); + conditionAttributes.put("typeReached", hint.getReachableType()); attributes.put("condition", conditionAttributes); } } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java new file mode 100644 index 000000000000..782497dee8b4 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/aot/nativex/RuntimeHintsWriter.java @@ -0,0 +1,66 @@ +/* + * 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.aot.nativex; + +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; + +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.core.SpringVersion; + +/** + * Write a {@link RuntimeHints} instance to the JSON output expected by the + * GraalVM {@code native-image} compiler, typically named {@code reachability-metadata.json}. + * + * @author Brian Clozel + * @since 7.0 + * @see GraalVM Reachability Metadata + */ +class RuntimeHintsWriter { + + public void write(BasicJsonWriter writer, RuntimeHints hints) { + Map document = new LinkedHashMap<>(); + String springVersion = SpringVersion.getVersion(); + if (springVersion != null) { + document.put("comment", "Spring Framework " + springVersion); + } + List> reflection = new ReflectionHintsAttributes().reflection(hints); + if (!reflection.isEmpty()) { + document.put("reflection", reflection); + } + List> jni = new ReflectionHintsAttributes().jni(hints); + if (!jni.isEmpty()) { + document.put("jni", jni); + } + List> resourceHints = new ResourceHintsAttributes().resources(hints.resources()); + if (!resourceHints.isEmpty()) { + document.put("resources", resourceHints); + } + List> resourceBundles = new ResourceHintsAttributes().resourceBundles(hints.resources()); + if (!resourceBundles.isEmpty()) { + document.put("bundles", resourceBundles); + } + List> serialization = new SerializationHintsAttributes().toAttributes(hints.serialization()); + if (!serialization.isEmpty()) { + document.put("serialization", serialization); + } + + writer.writeObject(document); + } + +} diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java b/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsAttributes.java similarity index 70% rename from spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java rename to spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsAttributes.java index 73b25248519e..d9ca1d6d5fd3 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsAttributes.java @@ -18,6 +18,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; +import java.util.List; import java.util.Map; import org.springframework.aot.hint.ConditionalHint; @@ -25,40 +26,36 @@ import org.springframework.aot.hint.SerializationHints; /** - * Write a {@link SerializationHints} to the JSON output expected by the - * GraalVM {@code native-image} compiler, typically named - * {@code serialization-config.json}. + * Collect {@link SerializationHints} as map attributes ready for JSON serialization for the GraalVM + * {@code native-image} compiler. * * @author Sebastien Deleuze * @author Stephane Nicoll * @author Brian Clozel - * @since 6.0 - * @see Native Image Build Configuration + * @see Native Image Build Configuration */ -class SerializationHintsWriter { - - public static final SerializationHintsWriter INSTANCE = new SerializationHintsWriter(); +class SerializationHintsAttributes { private static final Comparator JAVA_SERIALIZATION_HINT_COMPARATOR = Comparator.comparing(JavaSerializationHint::getType); - public void write(BasicJsonWriter writer, SerializationHints hints) { - writer.writeArray(hints.javaSerializationHints() + public List> toAttributes(SerializationHints hints) { + return hints.javaSerializationHints() .sorted(JAVA_SERIALIZATION_HINT_COMPARATOR) - .map(this::toAttributes).toList()); + .map(this::toAttributes).toList(); } private Map toAttributes(JavaSerializationHint serializationHint) { LinkedHashMap attributes = new LinkedHashMap<>(); handleCondition(attributes, serializationHint); - attributes.put("name", serializationHint.getType()); + attributes.put("type", serializationHint.getType()); return attributes; } private void handleCondition(Map attributes, ConditionalHint hint) { if (hint.getReachableType() != null) { Map conditionAttributes = new LinkedHashMap<>(); - conditionAttributes.put("typeReachable", hint.getReachableType()); + conditionAttributes.put("typeReached", hint.getReachableType()); attributes.put("condition", conditionAttributes); } } diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/feature/package-info.java b/spring-core/src/main/java/org/springframework/aot/nativex/feature/package-info.java index db71c8c1a6f7..11e2830acb7d 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/feature/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/feature/package-info.java @@ -1,9 +1,4 @@ /** * GraalVM native image features, not part of Spring Framework public API. */ -@NonNullApi -@NonNullFields package org.springframework.aot.nativex.feature; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java b/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java index 4cbc5065727e..056c804af7a8 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/package-info.java @@ -1,9 +1,7 @@ /** * Support for generating GraalVM native configuration from runtime hints. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot.nativex; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/aot/nativex/substitution/package-info.java b/spring-core/src/main/java/org/springframework/aot/nativex/substitution/package-info.java index dca0e7c5600d..e095edccc660 100644 --- a/spring-core/src/main/java/org/springframework/aot/nativex/substitution/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/nativex/substitution/package-info.java @@ -1,9 +1,4 @@ /** * GraalVM native image substitutions, not part of Spring Framework public API. */ -@NonNullApi -@NonNullFields package org.springframework.aot.nativex.substitution; - -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; diff --git a/spring-core/src/main/java/org/springframework/aot/package-info.java b/spring-core/src/main/java/org/springframework/aot/package-info.java index 36bce0751c66..4ca9a01ba13b 100644 --- a/spring-core/src/main/java/org/springframework/aot/package-info.java +++ b/spring-core/src/main/java/org/springframework/aot/package-info.java @@ -1,9 +1,7 @@ /** * Core package for Spring AOT infrastructure. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.aot; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/asm/ClassReader.java b/spring-core/src/main/java/org/springframework/asm/ClassReader.java index dca87bcc3207..8ae1e208bb51 100644 --- a/spring-core/src/main/java/org/springframework/asm/ClassReader.java +++ b/spring-core/src/main/java/org/springframework/asm/ClassReader.java @@ -195,7 +195,7 @@ public ClassReader( this.b = classFileBuffer; // Check the class' major_version. This field is after the magic and minor_version fields, which // use 4 and 2 bytes respectively. - if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V24) { + if (checkClassVersion && readShort(classFileOffset + 6) > Opcodes.V25) { throw new IllegalArgumentException( "Unsupported class file major version " + readShort(classFileOffset + 6)); } diff --git a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java index 6016a766a7ff..7fc511d4e625 100644 --- a/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java +++ b/spring-core/src/main/java/org/springframework/asm/MethodVisitor.java @@ -594,7 +594,7 @@ public void visitTableSwitchInsn( * Visits a LOOKUPSWITCH instruction. * * @param dflt beginning of the default handler block. - * @param keys the values of the keys. + * @param keys the values of the keys. Keys must be sorted in increasing order. * @param labels beginnings of the handler blocks. {@code labels[i]} is the beginning of the * handler block for the {@code keys[i]} key. */ diff --git a/spring-core/src/main/java/org/springframework/asm/Opcodes.java b/spring-core/src/main/java/org/springframework/asm/Opcodes.java index 69192d1aa758..c912933444d6 100644 --- a/spring-core/src/main/java/org/springframework/asm/Opcodes.java +++ b/spring-core/src/main/java/org/springframework/asm/Opcodes.java @@ -289,6 +289,7 @@ public interface Opcodes { int V22 = 0 << 16 | 66; int V23 = 0 << 16 | 67; int V24 = 0 << 16 | 68; + int V25 = 0 << 16 | 69; /** * Version flag indicating that the class is using 'preview' features. diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java index 52b94fd5d813..e3691feee77d 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanCopier.java @@ -116,6 +116,8 @@ public void generateClass(ClassVisitor v) { Type sourceType = Type.getType(source); Type targetType = Type.getType(target); ClassEmitter ce = new ClassEmitter(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java index 9969d511e991..78a85c4cdb85 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanGenerator.java @@ -117,6 +117,8 @@ public void generateClass(ClassVisitor v) throws Exception { types[i] = (Type)props.get(names[i]); } ClassEmitter ce = new ClassEmitter(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java index b1e3596f8ff5..ef9dcf0bc29d 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BeanMapEmitter.java @@ -57,6 +57,8 @@ class BeanMapEmitter extends ClassEmitter { public BeanMapEmitter(ClassVisitor v, String className, Class type, int require) { super(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . begin_class(Constants.V1_8, Constants.ACC_PUBLIC, className, BEAN_MAP, null, Constants.SOURCE_FILE); EmitUtils.null_constructor(this); EmitUtils.factory_method(this, NEW_INSTANCE); diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java index 8a6198f4c519..8a8dc5f17005 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/BulkBeanEmitter.java @@ -56,6 +56,8 @@ public BulkBeanEmitter(ClassVisitor v, Method[] setters = new Method[setterNames.length]; validate(target, getterNames, setterNames, types, getters, setters); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . begin_class(Constants.V1_8, Constants.ACC_PUBLIC, className, BULK_BEAN, null, Constants.SOURCE_FILE); EmitUtils.null_constructor(this); generateGet(target, getters); diff --git a/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java b/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java index 9c023394bd3a..4d64c5c71a1f 100644 --- a/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java +++ b/spring-core/src/main/java/org/springframework/cglib/beans/ImmutableBean.java @@ -89,6 +89,8 @@ public Object create() { public void generateClass(ClassVisitor v) { Type targetType = Type.getType(target); ClassEmitter ce = new ClassEmitter(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java b/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java index 9bee25e1c7ba..f4133915941c 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/KeyFactory.java @@ -261,6 +261,8 @@ public void generateClass(ClassVisitor v) { } Type[] parameterTypes = TypeUtils.getTypes(newInstance.getParameterTypes()); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java index 102f333c074b..bbbdcbaeee32 100644 --- a/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java +++ b/spring-core/src/main/java/org/springframework/cglib/core/ReflectUtils.java @@ -463,10 +463,21 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader, c = lookup.defineClass(b); } catch (LinkageError | IllegalArgumentException ex) { - // in case of plain LinkageError (class already defined) - // or IllegalArgumentException (class in different package): - // fall through to traditional ClassLoader.defineClass below - t = ex; + if (ex instanceof LinkageError) { + // Could be a ClassLoader mismatch with the class pre-existing in a + // parent ClassLoader -> try loadClass before giving up completely. + try { + c = contextClass.getClassLoader().loadClass(className); + } + catch (ClassNotFoundException cnfe) { + } + } + if (c == null) { + // in case of plain LinkageError (class already defined) + // or IllegalArgumentException (class in different package): + // fall through to traditional ClassLoader.defineClass below + t = ex; + } } catch (Throwable ex) { throw new CodeGenerationException(ex); @@ -527,15 +538,26 @@ public static Class defineClass(String className, byte[] b, ClassLoader loader, c = lookup.defineClass(b); } catch (LinkageError | IllegalAccessException ex) { - throw new CodeGenerationException(ex) { - @Override - public String getMessage() { - return "ClassLoader mismatch for [" + contextClass.getName() + - "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + - "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + - "; consider co-locating the affected class in that target ClassLoader instead."; + if (ex instanceof LinkageError) { + // Could be a ClassLoader mismatch with the class pre-existing in a + // parent ClassLoader -> try loadClass before giving up completely. + try { + c = contextClass.getClassLoader().loadClass(className); + } + catch (ClassNotFoundException cnfe) { } - }; + } + if (c == null) { + throw new CodeGenerationException(ex) { + @Override + public String getMessage() { + return "ClassLoader mismatch for [" + contextClass.getName() + + "]: JVM should be started with --add-opens=java.base/java.lang=ALL-UNNAMED " + + "for ClassLoader.defineClass to be accessible on " + loader.getClass().getName() + + "; consider co-locating the affected class in that target ClassLoader instead."; + } + }; + } } catch (Throwable ex) { throw new CodeGenerationException(ex); diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java b/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java index fc655f244ad2..3acd2fd69fe9 100644 --- a/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/Enhancer.java @@ -678,6 +678,8 @@ public void generateClass(ClassVisitor v) throws Exception { ClassEmitter e = new ClassEmitter(v); if (currentData == null) { + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . e.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), @@ -688,6 +690,8 @@ public void generateClass(ClassVisitor v) throws Exception { Constants.SOURCE_FILE); } else { + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . e.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/InterfaceMaker.java b/spring-core/src/main/java/org/springframework/cglib/proxy/InterfaceMaker.java index 5e61136a2a79..842c57fbe7a4 100644 --- a/spring-core/src/main/java/org/springframework/cglib/proxy/InterfaceMaker.java +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/InterfaceMaker.java @@ -94,23 +94,25 @@ public Class create() { } @Override - protected ClassLoader getDefaultClassLoader() { + protected ClassLoader getDefaultClassLoader() { return null; } @Override - protected Object firstInstance(Class type) { + protected Object firstInstance(Class type) { return type; } @Override - protected Object nextInstance(Object instance) { + protected Object nextInstance(Object instance) { throw new IllegalStateException("InterfaceMaker does not cache"); } @Override - public void generateClass(ClassVisitor v) throws Exception { + public void generateClass(ClassVisitor v) throws Exception { ClassEmitter ce = new ClassEmitter(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC | Constants.ACC_INTERFACE | Constants.ACC_ABSTRACT, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/proxy/MixinEmitter.java b/spring-core/src/main/java/org/springframework/cglib/proxy/MixinEmitter.java index ab2460fe9f8c..d34efd4e9910 100644 --- a/spring-core/src/main/java/org/springframework/cglib/proxy/MixinEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/proxy/MixinEmitter.java @@ -48,6 +48,8 @@ class MixinEmitter extends ClassEmitter { public MixinEmitter(ClassVisitor v, String className, Class[] classes, int[] route) { super(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . begin_class(Constants.V1_8, Constants.ACC_PUBLIC, className, diff --git a/spring-core/src/main/java/org/springframework/cglib/reflect/ConstructorDelegate.java b/spring-core/src/main/java/org/springframework/cglib/reflect/ConstructorDelegate.java index 5bb1c4bbff7d..7ba597713c28 100644 --- a/spring-core/src/main/java/org/springframework/cglib/reflect/ConstructorDelegate.java +++ b/spring-core/src/main/java/org/springframework/cglib/reflect/ConstructorDelegate.java @@ -105,6 +105,8 @@ public void generateClass(ClassVisitor v) { } ClassEmitter ce = new ClassEmitter(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/reflect/FastClassEmitter.java b/spring-core/src/main/java/org/springframework/cglib/reflect/FastClassEmitter.java index 4e5ba38b5c13..2375f3783094 100644 --- a/spring-core/src/main/java/org/springframework/cglib/reflect/FastClassEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/reflect/FastClassEmitter.java @@ -75,6 +75,8 @@ public FastClassEmitter(ClassVisitor v, String className, Class type) { super(v); Type base = Type.getType(type); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . begin_class(Constants.V1_8, Constants.ACC_PUBLIC, className, FAST_CLASS, null, Constants.SOURCE_FILE); // constructor diff --git a/spring-core/src/main/java/org/springframework/cglib/reflect/MethodDelegate.java b/spring-core/src/main/java/org/springframework/cglib/reflect/MethodDelegate.java index 3a7d1085b9c6..9cebc32d5c5a 100644 --- a/spring-core/src/main/java/org/springframework/cglib/reflect/MethodDelegate.java +++ b/spring-core/src/main/java/org/springframework/cglib/reflect/MethodDelegate.java @@ -236,6 +236,8 @@ public void generateClass(ClassVisitor v) throws NoSuchMethodException { ClassEmitter ce = new ClassEmitter(v); CodeEmitter e; + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/reflect/MulticastDelegate.java b/spring-core/src/main/java/org/springframework/cglib/reflect/MulticastDelegate.java index b23a8b172073..d0fc23ac6392 100644 --- a/spring-core/src/main/java/org/springframework/cglib/reflect/MulticastDelegate.java +++ b/spring-core/src/main/java/org/springframework/cglib/reflect/MulticastDelegate.java @@ -117,6 +117,8 @@ public void generateClass(ClassVisitor cv) { final MethodInfo method = ReflectUtils.getMethodInfo(ReflectUtils.findInterfaceMethod(iface)); ClassEmitter ce = new ClassEmitter(cv); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorterEmitter.java b/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorterEmitter.java index 2222ff6ae910..22bb96595f66 100644 --- a/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorterEmitter.java +++ b/spring-core/src/main/java/org/springframework/cglib/util/ParallelSorterEmitter.java @@ -38,6 +38,8 @@ class ParallelSorterEmitter extends ClassEmitter { public ParallelSorterEmitter(ClassVisitor v, String className, Object[] arrays) { super(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . begin_class(Constants.V1_8, Constants.ACC_PUBLIC, className, PARALLEL_SORTER, null, Constants.SOURCE_FILE); EmitUtils.null_constructor(this); EmitUtils.factory_method(this, NEW_INSTANCE); diff --git a/spring-core/src/main/java/org/springframework/cglib/util/StringSwitcher.java b/spring-core/src/main/java/org/springframework/cglib/util/StringSwitcher.java index 01f4d188e4f6..8cab210c20f9 100644 --- a/spring-core/src/main/java/org/springframework/cglib/util/StringSwitcher.java +++ b/spring-core/src/main/java/org/springframework/cglib/util/StringSwitcher.java @@ -133,6 +133,8 @@ public StringSwitcher create() { @Override public void generateClass(ClassVisitor v) throws Exception { ClassEmitter ce = new ClassEmitter(v); + // Byte code level cannot be higher than 1.8 due to STATICHOOK methods + // which set static final fields outside the initializer method . ce.begin_class(Constants.V1_8, Constants.ACC_PUBLIC, getClassName(), diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java index 3347e4a4e399..721ce6d6108b 100644 --- a/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessor.java @@ -18,7 +18,8 @@ import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -48,8 +49,7 @@ public interface AttributeAccessor { * @param name the unique attribute key * @return the current value of the attribute, if any */ - @Nullable - Object getAttribute(String name); + @Nullable Object getAttribute(String name); /** * Compute a new value for the attribute identified by {@code name} if @@ -89,8 +89,7 @@ default T computeAttribute(String name, Function computeFunction) * @param name the unique attribute key * @return the last value of the attribute, if any */ - @Nullable - Object removeAttribute(String name); + @Nullable Object removeAttribute(String name); /** * Return {@code true} if the attribute identified by {@code name} exists. diff --git a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java index 87e1975a82b6..c1645140b760 100644 --- a/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java +++ b/spring-core/src/main/java/org/springframework/core/AttributeAccessorSupport.java @@ -21,7 +21,8 @@ import java.util.Map; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -55,8 +56,7 @@ public void setAttribute(String name, @Nullable Object value) { } @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { Assert.notNull(name, "Name must not be null"); return this.attributes.get(name); } @@ -73,8 +73,7 @@ public T computeAttribute(String name, Function computeFunction) } @Override - @Nullable - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { Assert.notNull(name, "Name must not be null"); return this.attributes.remove(name); } diff --git a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java index a5c666202515..934f040e19d0 100644 --- a/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java +++ b/spring-core/src/main/java/org/springframework/core/BridgeMethodResolver.java @@ -24,7 +24,8 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; @@ -148,8 +149,7 @@ private static boolean isBridgedCandidateFor(Method candidateMethod, Method brid * @param bridgeMethod the bridge method * @return the bridged method, or {@code null} if none found */ - @Nullable - private static Method searchCandidates(List candidateMethods, Method bridgeMethod) { + private static @Nullable Method searchCandidates(List candidateMethods, Method bridgeMethod) { if (candidateMethods.isEmpty()) { return null; } @@ -215,8 +215,7 @@ private static boolean isResolvedTypeMatch(Method genericMethod, Method candidat * matches that of the supplied bridge method. * @throws IllegalStateException if the generic declaration cannot be found */ - @Nullable - private static Method findGenericDeclaration(Method bridgeMethod) { + private static @Nullable Method findGenericDeclaration(Method bridgeMethod) { if (!bridgeMethod.isBridge()) { return bridgeMethod; } @@ -235,8 +234,7 @@ private static Method findGenericDeclaration(Method bridgeMethod) { return searchInterfaces(interfaces, bridgeMethod); } - @Nullable - private static Method searchInterfaces(Class[] interfaces, Method bridgeMethod) { + private static @Nullable Method searchInterfaces(Class[] interfaces, Method bridgeMethod) { for (Class ifc : interfaces) { Method method = searchForMatch(ifc, bridgeMethod); if (method != null && !method.isBridge()) { @@ -257,8 +255,7 @@ private static Method searchInterfaces(Class[] interfaces, Method bridgeMetho * that of the supplied {@link Method}, then this matching {@link Method} is returned, * otherwise {@code null} is returned. */ - @Nullable - private static Method searchForMatch(Class type, Method bridgeMethod) { + private static @Nullable Method searchForMatch(Class type, Method bridgeMethod) { try { return type.getDeclaredMethod(bridgeMethod.getName(), bridgeMethod.getParameterTypes()); } diff --git a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java index 83af2d54dd4e..ebdc5b7cd2b4 100644 --- a/spring-core/src/main/java/org/springframework/core/CollectionFactory.java +++ b/spring-core/src/main/java/org/springframework/core/CollectionFactory.java @@ -36,7 +36,8 @@ import java.util.TreeMap; import java.util.TreeSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -349,8 +350,7 @@ else if (HashMap.class == mapType) { public static Properties createStringAdaptingProperties() { return new SortedProperties(false) { @Override - @Nullable - public String getProperty(String key) { + public @Nullable String getProperty(String key) { Object value = get(key); return (value != null ? value.toString() : null); } diff --git a/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java index eb845e47977d..60425774d592 100644 --- a/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java +++ b/spring-core/src/main/java/org/springframework/core/ConfigurableObjectInputStream.java @@ -22,7 +22,8 @@ import java.io.ObjectInputStream; import java.io.ObjectStreamClass; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** @@ -35,8 +36,7 @@ */ public class ConfigurableObjectInputStream extends ObjectInputStream { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final boolean acceptProxyClasses; @@ -144,8 +144,7 @@ protected Class resolveFallbackIfPossible(String className, ClassNotFoundExce *

The default implementation simply returns {@code null}, indicating * that no specific fallback is available. */ - @Nullable - protected ClassLoader getFallbackClassLoader() throws IOException { + protected @Nullable ClassLoader getFallbackClassLoader() throws IOException { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/Constants.java b/spring-core/src/main/java/org/springframework/core/Constants.java index 7e794fe4d288..e7456bef7950 100644 --- a/spring-core/src/main/java/org/springframework/core/Constants.java +++ b/spring-core/src/main/java/org/springframework/core/Constants.java @@ -23,7 +23,8 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; diff --git a/spring-core/src/main/java/org/springframework/core/Conventions.java b/spring-core/src/main/java/org/springframework/core/Conventions.java index eae2f796fb29..88aeba5f0295 100644 --- a/spring-core/src/main/java/org/springframework/core/Conventions.java +++ b/spring-core/src/main/java/org/springframework/core/Conventions.java @@ -21,7 +21,8 @@ import java.util.Collection; import java.util.Iterator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; 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 48efd73d748e..f53847044d8c 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 @@ 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; @@ -41,11 +42,11 @@ import kotlinx.coroutines.flow.Flow; import kotlinx.coroutines.reactor.MonoKt; import kotlinx.coroutines.reactor.ReactorFlowKt; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -131,11 +132,7 @@ public static Publisher invokeSuspendingFunction( 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 static Publisher invokeSuspendingFunction( 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/DecoratingClassLoader.java b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java index 787c1e14e390..24a1f60034e6 100644 --- a/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/DecoratingClassLoader.java @@ -19,7 +19,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** 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 70d6530b285d..3b77fcd1e6f0 100644 --- a/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java +++ b/spring-core/src/main/java/org/springframework/core/GenericTypeResolver.java @@ -25,7 +25,8 @@ import java.util.HashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -58,9 +59,9 @@ private 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"); @@ -90,8 +91,7 @@ public static Class resolveReturnType(Method method, Class clazz) { * @return the resolved parameter type of the method return type, or {@code null} * if not resolvable or if the single argument is of type {@link WildcardType}. */ - @Nullable - public static Class resolveReturnTypeArgument(Method method, Class genericType) { + public static @Nullable Class resolveReturnTypeArgument(Method method, Class genericType) { Assert.notNull(method, "Method must not be null"); ResolvableType resolvableType = ResolvableType.forMethodReturnType(method).as(genericType); if (!resolvableType.hasGenerics() || resolvableType.getType() instanceof WildcardType) { @@ -108,8 +108,7 @@ public static Class resolveReturnTypeArgument(Method method, Class generic * @param genericType the generic interface or superclass to resolve the type argument from * @return the resolved type of the argument, or {@code null} if not resolvable */ - @Nullable - public static Class resolveTypeArgument(Class clazz, Class genericType) { + public static @Nullable Class resolveTypeArgument(Class clazz, Class genericType) { ResolvableType resolvableType = ResolvableType.forClass(clazz).as(genericType); if (!resolvableType.hasGenerics()) { return null; @@ -117,8 +116,7 @@ public static Class resolveTypeArgument(Class clazz, Class genericType) return getSingleGeneric(resolvableType); } - @Nullable - private static Class getSingleGeneric(ResolvableType resolvableType) { + private static @Nullable Class getSingleGeneric(ResolvableType resolvableType) { Assert.isTrue(resolvableType.getGenerics().length == 1, () -> "Expected 1 type argument on generic interface [" + resolvableType + "] but found " + resolvableType.getGenerics().length); @@ -135,8 +133,7 @@ private static Class getSingleGeneric(ResolvableType resolvableType) { * @return the resolved type of each argument, with the array size matching the * number of actual type arguments, or {@code null} if not resolvable */ - @Nullable - public static Class[] resolveTypeArguments(Class clazz, Class genericType) { + public static Class @Nullable [] resolveTypeArguments(Class clazz, Class genericType) { ResolvableType type = ResolvableType.forClass(clazz).as(genericType); if (!type.hasGenerics() || !type.hasResolvableGenerics()) { return null; @@ -304,8 +301,7 @@ public TypeVariableMapVariableResolver(Map typeVariableMap) } @Override - @Nullable - public ResolvableType resolveVariable(TypeVariable variable) { + public @Nullable ResolvableType resolveVariable(TypeVariable variable) { Type type = this.typeVariableMap.get(variable); return (type != null ? ResolvableType.forType(type) : null); } diff --git a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java index 696ee372b00e..3e104670d007 100644 --- a/spring-core/src/main/java/org/springframework/core/KotlinDetector.java +++ b/spring-core/src/main/java/org/springframework/core/KotlinDetector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,13 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** - * A common delegate for detecting Kotlin's presence and for identifying Kotlin types. + * A common delegate for detecting Kotlin's presence and for identifying Kotlin types. All the methods of this class + * can be safely used without any preliminary classpath checks. * * @author Juergen Hoeller * @author Sebastien Deleuze @@ -32,11 +34,11 @@ @SuppressWarnings("unchecked") public abstract class KotlinDetector { - @Nullable - private static final Class kotlinMetadata; + private static final @Nullable Class kotlinMetadata; + + private static final @Nullable Class kotlinJvmInline; - @Nullable - private static final Class kotlinJvmInline; + private static final @Nullable Class kotlinCoroutineContinuation; // For ConstantFieldFeature compliance, otherwise could be deduced from kotlinMetadata private static final boolean kotlinPresent; @@ -47,6 +49,7 @@ public abstract class KotlinDetector { ClassLoader classLoader = KotlinDetector.class.getClassLoader(); Class metadata = null; Class jvmInline = null; + Class coroutineContinuation = null; try { metadata = ClassUtils.forName("kotlin.Metadata", classLoader); try { @@ -55,14 +58,21 @@ public abstract class KotlinDetector { catch (ClassNotFoundException ex) { // JVM inline support not available } + try { + coroutineContinuation = ClassUtils.forName("kotlin.coroutines.Continuation", classLoader); + } + catch (ClassNotFoundException ex) { + // Coroutines support not available + } } catch (ClassNotFoundException ex) { // Kotlin API not available - no Kotlin support } kotlinMetadata = (Class) metadata; kotlinPresent = (kotlinMetadata != null); - kotlinReflectPresent = kotlinPresent && ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); + kotlinReflectPresent = ClassUtils.isPresent("kotlin.reflect.full.KClasses", classLoader); kotlinJvmInline = (Class) jvmInline; + kotlinCoroutineContinuation = coroutineContinuation; } @@ -90,7 +100,7 @@ public static boolean isKotlinReflectPresent() { * as invokedynamic has become the default method for lambda generation. */ public static boolean isKotlinType(Class clazz) { - return (kotlinMetadata != null && clazz.getDeclaredAnnotation(kotlinMetadata) != null); + return (kotlinPresent && clazz.getDeclaredAnnotation(kotlinMetadata) != null); } /** @@ -98,13 +108,11 @@ public static boolean isKotlinType(Class clazz) { * @since 5.3 */ public static boolean isSuspendingFunction(Method method) { - if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { - Class[] types = method.getParameterTypes(); - if (types.length > 0 && "kotlin.coroutines.Continuation".equals(types[types.length - 1].getName())) { - return true; - } + if (kotlinCoroutineContinuation == null) { + return false; } - return false; + int parameterCount = method.getParameterCount(); + return (parameterCount > 0 && method.getParameterTypes()[parameterCount - 1] == kotlinCoroutineContinuation); } /** 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 485e289444df..0e8493d2f0cb 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. @@ -23,8 +23,7 @@ import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.jvm.ReflectJvmMapping; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link ParameterNameDiscoverer} implementation which uses Kotlin's reflection facilities @@ -41,40 +40,35 @@ public class KotlinReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { @Override - @Nullable - public String[] 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; + public @Nullable String @Nullable [] getParameterNames(Method method) { + if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { + try { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + return (function != null ? getParameterNames(function.getParameters()) : null); + } + catch (UnsupportedOperationException ignored) { + } } + return null; } @Override - @Nullable - public String[] 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; + public @Nullable String @Nullable [] getParameterNames(Constructor ctor) { + 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; } - @Nullable - private String[] getParameterNames(List parameters) { - String[] parameterNames = parameters.stream() + private @Nullable String @Nullable [] getParameterNames(List parameters) { + @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/MethodClassKey.java b/spring-core/src/main/java/org/springframework/core/MethodClassKey.java index b7a450e49e38..b7efe2ab2dea 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodClassKey.java +++ b/spring-core/src/main/java/org/springframework/core/MethodClassKey.java @@ -18,7 +18,8 @@ import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** @@ -33,8 +34,7 @@ public final class MethodClassKey implements Comparable { private final Method method; - @Nullable - private final Class targetClass; + private final @Nullable Class targetClass; /** diff --git a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java index f1ceac4a7ccf..91f9e4534768 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java +++ b/spring-core/src/main/java/org/springframework/core/MethodIntrospector.java @@ -23,7 +23,8 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -155,8 +156,7 @@ public interface MetadataLookup { * @return non-null metadata to be associated with a method if there is a match, * or {@code null} for no match */ - @Nullable - T inspect(Method method); + @Nullable T inspect(Method method); } } 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 fd2d666fe1a4..aa5acd48a445 100644 --- a/spring-core/src/main/java/org/springframework/core/MethodParameter.java +++ b/spring-core/src/main/java/org/springframework/core/MethodParameter.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. @@ -38,8 +38,8 @@ import kotlin.reflect.KFunction; import kotlin.reflect.KParameter; import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -71,36 +71,27 @@ public class MethodParameter { private final int parameterIndex; - @Nullable - private volatile Parameter parameter; + private volatile @Nullable Parameter parameter; private int nestingLevel; /** Map from Integer level to Integer type index. */ - @Nullable - Map typeIndexesPerLevel; + @Nullable Map typeIndexesPerLevel; /** The containing class. Could also be supplied by overriding {@link #getContainingClass()} */ - @Nullable - private volatile Class containingClass; + private volatile @Nullable Class containingClass; - @Nullable - private volatile Class parameterType; + private volatile @Nullable Class parameterType; - @Nullable - private volatile Type genericParameterType; + private volatile @Nullable Type genericParameterType; - @Nullable - private volatile Annotation[] parameterAnnotations; + private volatile Annotation @Nullable [] parameterAnnotations; - @Nullable - private volatile ParameterNameDiscoverer parameterNameDiscoverer; + private volatile @Nullable ParameterNameDiscoverer parameterNameDiscoverer; - @Nullable - volatile String parameterName; + volatile @Nullable String parameterName; - @Nullable - private volatile MethodParameter nestedMethodParameter; + private volatile @Nullable MethodParameter nestedMethodParameter; /** @@ -197,8 +188,7 @@ public MethodParameter(MethodParameter original) { *

Note: Either Method or Constructor is available. * @return the Method, or {@code null} if none */ - @Nullable - public Method getMethod() { + public @Nullable Method getMethod() { return (this.executable instanceof Method method ? method : null); } @@ -207,8 +197,7 @@ public Method getMethod() { *

Note: Either Method or Constructor is available. * @return the Constructor, or {@code null} if none */ - @Nullable - public Constructor getConstructor() { + public @Nullable Constructor getConstructor() { return (this.executable instanceof Constructor constructor ? constructor : null); } @@ -275,9 +264,9 @@ public int getParameterIndex() { /** * 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++; } @@ -285,10 +274,10 @@ public void increaseNestingLevel() { /** * 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--; @@ -318,9 +307,9 @@ public MethodParameter withTypeIndex(int typeIndex) { * @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); } @@ -331,8 +320,7 @@ public void setTypeIndexForCurrentLevel(int typeIndex) { * if none specified (indicating the default type index) * @see #getNestingLevel() */ - @Nullable - public Integer getTypeIndexForCurrentLevel() { + public @Nullable Integer getTypeIndexForCurrentLevel() { return getTypeIndexForLevel(this.nestingLevel); } @@ -342,8 +330,7 @@ public Integer getTypeIndexForCurrentLevel() { * @return the corresponding type index, or {@code null} * if none specified (indicating the default type index) */ - @Nullable - public Integer getTypeIndexForLevel(int nestingLevel) { + public @Nullable Integer getTypeIndexForLevel(int nestingLevel) { return getTypeIndexesPerLevel().get(nestingLevel); } @@ -400,31 +387,16 @@ private MethodParameter nested(int nestingLevel, @Nullable Integer typeIndex) { /** * Return whether this method indicates a parameter which is not required: - * either in the form of Java 8's {@link java.util.Optional}, any variant - * of a parameter-level {@code Nullable} annotation (such as from JSR-305 - * or the FindBugs set of annotations), or a language-level nullable type + * either in the form of Java 8's {@link java.util.Optional}, JSpecify annotations, + * any variant of a parameter-level {@code @Nullable} annotation (such as from Spring, + * JSR-305 or Jakarta set of annotations), a language-level nullable type * declaration or {@code Continuation} parameter in Kotlin. * @since 4.3 + * @see Nullness#forMethodParameter(MethodParameter) */ public boolean isOptional() { - return (getParameterType() == Optional.class || hasNullableAnnotation() || - (KotlinDetector.isKotlinReflectPresent() && - KotlinDetector.isKotlinType(getContainingClass()) && - KotlinDelegate.isOptional(this))); - } - - /** - * Check whether this method parameter 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 : getParameterAnnotations()) { - if ("Nullable".equals(ann.annotationType().getSimpleName())) { - return true; - } - } - return false; + return (getParameterType() == Optional.class || Nullness.forMethodParameter(this) == Nullness.NULLABLE || + (KotlinDetector.isKotlinType(getContainingClass()) && KotlinDelegate.isOptional(this))); } /** @@ -457,7 +429,7 @@ public MethodParameter withContainingClass(@Nullable Class containingClass) { /** * 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; @@ -477,7 +449,7 @@ public Class getContainingClass() { /** * Set a resolved (generic) parameter type. */ - @Deprecated + @Deprecated(since = "5.2") void setParameterType(@Nullable Class parameterType) { this.parameterType = parameterType; } @@ -512,8 +484,8 @@ public Type getGenericParameterType() { if (this.parameterIndex < 0) { Method method = getMethod(); paramType = (method != null ? - (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass()) ? - KotlinDelegate.getGenericReturnType(method) : method.getGenericReturnType()) : void.class); + (KotlinDetector.isKotlinType(getContainingClass()) ? + KotlinDelegate.getGenericReturnType(method) : method.getGenericReturnType()) : void.class); } else { Type[] genericParameterTypes = this.executable.getGenericParameterTypes(); @@ -540,7 +512,7 @@ private Class computeParameterType() { if (method == null) { return void.class; } - if (KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(getContainingClass())) { + if (KotlinDetector.isKotlinType(getContainingClass())) { return KotlinDelegate.getReturnType(method); } return method.getReturnType(); @@ -616,8 +588,7 @@ public Annotation[] getMethodAnnotations() { * @param annotationType the annotation type to look for * @return the annotation object, or {@code null} if not found */ - @Nullable - public A getMethodAnnotation(Class annotationType) { + public @Nullable A getMethodAnnotation(Class annotationType) { A annotation = getAnnotatedElement().getAnnotation(annotationType); return (annotation != null ? adaptAnnotation(annotation) : null); } @@ -669,8 +640,7 @@ public boolean hasParameterAnnotations() { * @return the annotation object, or {@code null} if not found */ @SuppressWarnings("unchecked") - @Nullable - public A getParameterAnnotation(Class annotationType) { + public @Nullable A getParameterAnnotation(Class annotationType) { Annotation[] anns = getParameterAnnotations(); for (Annotation ann : anns) { if (annotationType.isInstance(ann)) { @@ -706,14 +676,13 @@ public void initParameterNameDiscovery(@Nullable ParameterNameDiscoverer paramet * {@link #initParameterNameDiscovery ParameterNameDiscoverer} * has been set to begin with) */ - @Nullable - public String getParameterName() { + public @Nullable String getParameterName() { if (this.parameterIndex < 0) { return null; } ParameterNameDiscoverer discoverer = this.parameterNameDiscoverer; if (discoverer != null) { - String[] parameterNames = null; + @Nullable String[] parameterNames = null; if (this.executable instanceof Method method) { parameterNames = discoverer.getParameterNames(method); } @@ -789,9 +758,9 @@ public MethodParameter clone() { * @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( @@ -872,7 +841,7 @@ private static int validateIndex(Executable executable, int parameterIndex) { * @return the corresponding MethodParameter instance * @since 6.1 */ - public static MethodParameter forFieldAwareConstructor(Constructor ctor, int parameterIndex, String fieldName) { + public static MethodParameter forFieldAwareConstructor(Constructor ctor, int parameterIndex, @Nullable String fieldName) { return new FieldAwareConstructorParameter(ctor, parameterIndex, fieldName); } @@ -882,10 +851,9 @@ public static MethodParameter forFieldAwareConstructor(Constructor ctor, int */ private static class FieldAwareConstructorParameter extends MethodParameter { - @Nullable - private volatile Annotation[] combinedAnnotations; + private volatile Annotation @Nullable [] combinedAnnotations; - public FieldAwareConstructorParameter(Constructor constructor, int parameterIndex, String fieldName) { + public FieldAwareConstructorParameter(Constructor constructor, int parameterIndex, @Nullable String fieldName) { super(constructor, parameterIndex); this.parameterName = fieldName; } diff --git a/spring-core/src/main/java/org/springframework/core/NativeDetector.java b/spring-core/src/main/java/org/springframework/core/NativeDetector.java index 46d72b7bdd7e..87f2f83c0fd0 100644 --- a/spring-core/src/main/java/org/springframework/core/NativeDetector.java +++ b/spring-core/src/main/java/org/springframework/core/NativeDetector.java @@ -16,7 +16,7 @@ package org.springframework.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A common delegate for detecting a GraalVM native image environment. @@ -27,8 +27,7 @@ public abstract class NativeDetector { // See https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java - @Nullable - private static final String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode"); + private static final @Nullable String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode"); private static final boolean inNativeImage = (imageCode != null); diff --git a/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java b/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java index dcf25b5f5c04..1195c04f09ac 100644 --- a/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java +++ b/spring-core/src/main/java/org/springframework/core/NestedCheckedException.java @@ -16,7 +16,7 @@ package org.springframework.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Handy class for wrapping checked {@code Exceptions} with a root cause. @@ -60,8 +60,7 @@ public NestedCheckedException(@Nullable String msg, @Nullable Throwable cause) { * Retrieve the innermost cause of this exception, if any. * @return the innermost exception, or {@code null} if none */ - @Nullable - public Throwable getRootCause() { + public @Nullable Throwable getRootCause() { return NestedExceptionUtils.getRootCause(this); } diff --git a/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java b/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java index a2a596eacfc2..eba27406021e 100644 --- a/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java +++ b/spring-core/src/main/java/org/springframework/core/NestedExceptionUtils.java @@ -16,7 +16,7 @@ package org.springframework.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Helper class for implementing exception classes which are capable of @@ -41,8 +41,7 @@ public abstract class NestedExceptionUtils { * with selective inclusion of cause messages */ @Deprecated(since = "6.0") - @Nullable - public static String buildMessage(@Nullable String message, @Nullable Throwable cause) { + public static @Nullable String buildMessage(@Nullable String message, @Nullable Throwable cause) { if (cause == null) { return message; } @@ -60,8 +59,7 @@ public static String buildMessage(@Nullable String message, @Nullable Throwable * @return the innermost exception, or {@code null} if none * @since 4.3.9 */ - @Nullable - public static Throwable getRootCause(@Nullable Throwable original) { + public static @Nullable Throwable getRootCause(@Nullable Throwable original) { if (original == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java index bd6afe4d14cc..0a50f34b2aa9 100644 --- a/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java +++ b/spring-core/src/main/java/org/springframework/core/NestedRuntimeException.java @@ -16,7 +16,7 @@ package org.springframework.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Handy class for wrapping runtime {@code Exceptions} with a root cause. @@ -61,8 +61,7 @@ public NestedRuntimeException(@Nullable String msg, @Nullable Throwable cause) { * @return the innermost exception, or {@code null} if none * @since 2.0 */ - @Nullable - public Throwable getRootCause() { + public @Nullable Throwable getRootCause() { return NestedExceptionUtils.getRootCause(this); } diff --git a/spring-core/src/main/java/org/springframework/core/Nullness.java b/spring-core/src/main/java/org/springframework/core/Nullness.java new file mode 100644 index 000000000000..2c0f2f3036dc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/Nullness.java @@ -0,0 +1,224 @@ +/* + * Copyright 2002-2025 the original author 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; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.lang.reflect.AnnotatedType; +import java.lang.reflect.Constructor; +import java.lang.reflect.Executable; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.util.Objects; +import java.util.function.Predicate; + +import kotlin.reflect.KFunction; +import kotlin.reflect.KParameter; +import kotlin.reflect.KProperty; +import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + +/** + * Constants that indicate nullness, as well as related utility methods. + * + *

Nullness applies to type usage, a field, a method return type, or a parameter. + * JSpecify annotations are + * fully supported, as well as + * Kotlin null safety, + * {@code @Nullable} annotations regardless of their package, and Java primitive + * types. + * + *

JSR-305 annotations as well as Spring null safety annotations in the + * {@code org.springframework.lang} package such as {@code @NonNullApi}, + * {@code @NonNullFields}, and {@code @NonNull} are not supported by this API. + * However, {@code @Nullable} is supported via the package-less check. Migrating + * to JSpecify is recommended. + * + * @author Sebastien Deleuze + * @since 7.0 + */ +public enum Nullness { + + /** + * Unspecified nullness (Java default for non-primitive types and JSpecify + * {@code @NullUnmarked} code). + */ + UNSPECIFIED, + + /** + * Can include null (typically specified with a {@code @Nullable} annotation). + */ + NULLABLE, + + /** + * Will not include null (Kotlin default and JSpecify {@code @NullMarked} code). + */ + NON_NULL; + + + /** + * Return the nullness of the return type for the given method. + * @param method the source for the method return type + * @return the corresponding nullness + */ + public static Nullness forMethodReturnType(Method method) { + if (KotlinDetector.isKotlinType(method.getDeclaringClass())) { + return KotlinDelegate.forMethodReturnType(method); + } + return (hasNullableAnnotation(method) ? Nullness.NULLABLE : + jSpecifyNullness(method, method.getDeclaringClass(), method.getAnnotatedReturnType())); + } + + /** + * Return the nullness of the given parameter. + * @param parameter the parameter descriptor + * @return the corresponding nullness + */ + public static Nullness forParameter(Parameter parameter) { + if (KotlinDetector.isKotlinType(parameter.getDeclaringExecutable().getDeclaringClass())) { + // TODO Optimize when kotlin-reflect provide a more direct Parameter to KParameter resolution + MethodParameter methodParameter = MethodParameter.forParameter(parameter); + return KotlinDelegate.forParameter(methodParameter.getExecutable(), methodParameter.getParameterIndex()); + } + Executable executable = parameter.getDeclaringExecutable(); + return (hasNullableAnnotation(parameter) ? Nullness.NULLABLE : + jSpecifyNullness(executable, executable.getDeclaringClass(), parameter.getAnnotatedType())); + } + + /** + * Return the nullness of the given method parameter. + * @param methodParameter the method parameter descriptor + * @return the corresponding nullness + */ + public static Nullness forMethodParameter(MethodParameter methodParameter) { + return (methodParameter.getParameterIndex() < 0 ? + forMethodReturnType(Objects.requireNonNull(methodParameter.getMethod())) : + forParameter(methodParameter.getParameter())); + } + + /** + * Return the nullness of the given field. + * @param field the field descriptor + * @return the corresponding nullness + */ + public static Nullness forField(Field field) { + if (KotlinDetector.isKotlinType(field.getDeclaringClass())) { + return KotlinDelegate.forField(field); + } + return (hasNullableAnnotation(field) ? Nullness.NULLABLE : + jSpecifyNullness(field, field.getDeclaringClass(), field.getAnnotatedType())); + } + + + // Check method and parameter level @Nullable annotations regardless of the package + // (including Spring and JSR 305 annotations) + private static boolean hasNullableAnnotation(AnnotatedElement element) { + for (Annotation annotation : element.getDeclaredAnnotations()) { + if ("Nullable".equals(annotation.annotationType().getSimpleName())) { + return true; + } + } + return false; + } + + private static Nullness jSpecifyNullness( + AnnotatedElement annotatedElement, Class declaringClass, AnnotatedType annotatedType) { + + if (annotatedType.getType() instanceof Class clazz && clazz.isPrimitive()) { + return (clazz != void.class ? Nullness.NON_NULL : Nullness.UNSPECIFIED); + } + if (annotatedType.isAnnotationPresent(Nullable.class)) { + return Nullness.NULLABLE; + } + if (annotatedType.isAnnotationPresent(NonNull.class)) { + return Nullness.NON_NULL; + } + Nullness nullness = Nullness.UNSPECIFIED; + // Package level + Package declaringPackage = declaringClass.getPackage(); + if (declaringPackage.isAnnotationPresent(NullMarked.class)) { + nullness = Nullness.NON_NULL; + } + // Class level + if (declaringClass.isAnnotationPresent(NullMarked.class)) { + nullness = Nullness.NON_NULL; + } + else if (declaringClass.isAnnotationPresent(NullUnmarked.class)) { + nullness = Nullness.UNSPECIFIED; + } + // Annotated element level + if (annotatedElement.isAnnotationPresent(NullMarked.class)) { + nullness = Nullness.NON_NULL; + } + else if (annotatedElement.isAnnotationPresent(NullUnmarked.class)) { + nullness = Nullness.UNSPECIFIED; + } + return nullness; + } + + /** + * Inner class to avoid a hard dependency on Kotlin at runtime. + */ + private static class KotlinDelegate { + + public static Nullness forMethodReturnType(Method method) { + KFunction function = ReflectJvmMapping.getKotlinFunction(method); + if (function != null && function.getReturnType().isMarkedNullable()) { + return Nullness.NULLABLE; + } + return Nullness.NON_NULL; + } + + public static Nullness forParameter(Executable executable, int parameterIndex) { + KFunction function; + Predicate predicate; + if (executable instanceof Method method) { + function = ReflectJvmMapping.getKotlinFunction(method); + predicate = p -> KParameter.Kind.VALUE.equals(p.getKind()); + } + else { + function = ReflectJvmMapping.getKotlinFunction((Constructor) executable); + predicate = p -> (KParameter.Kind.VALUE.equals(p.getKind()) || + KParameter.Kind.INSTANCE.equals(p.getKind())); + } + if (function == null) { + return Nullness.UNSPECIFIED; + } + int i = 0; + for (KParameter kParameter : function.getParameters()) { + if (predicate.test(kParameter) && parameterIndex == i++) { + return (kParameter.getType().isMarkedNullable() ? Nullness.NULLABLE : Nullness.NON_NULL); + } + } + return Nullness.UNSPECIFIED; + } + + public static Nullness forField(Field field) { + KProperty property = ReflectJvmMapping.getKotlinProperty(field); + if (property != null && property.getReturnType().isMarkedNullable()) { + return Nullness.NULLABLE; + } + return Nullness.NON_NULL; + } + + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/OrderComparator.java b/spring-core/src/main/java/org/springframework/core/OrderComparator.java index 2724a414cf20..43b8409e8e9e 100644 --- a/spring-core/src/main/java/org/springframework/core/OrderComparator.java +++ b/spring-core/src/main/java/org/springframework/core/OrderComparator.java @@ -20,7 +20,8 @@ import java.util.Comparator; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** @@ -140,8 +141,7 @@ protected int getOrder(@Nullable Object obj) { * @param obj the object to check * @return the order value, or {@code null} if none found */ - @Nullable - protected Integer findOrder(Object obj) { + protected @Nullable Integer findOrder(Object obj) { return (obj instanceof Ordered ordered ? ordered.getOrder() : null); } @@ -156,8 +156,7 @@ protected Integer findOrder(Object obj) { * @return the priority value, or {@code null} if none * @since 4.1 */ - @Nullable - public Integer getPriority(Object obj) { + public @Nullable Integer getPriority(Object obj) { return null; } @@ -222,8 +221,7 @@ public interface OrderSourceProvider { * @param obj the object to find an order source for * @return the order source for that object, or {@code null} if none found */ - @Nullable - Object getOrderSource(Object obj); + @Nullable Object getOrderSource(Object obj); } } diff --git a/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java index 63aeab629aae..15c8c7cca98a 100644 --- a/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/OverridingClassLoader.java @@ -19,7 +19,8 @@ import java.io.IOException; import java.io.InputStream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.FileCopyUtils; /** @@ -47,8 +48,7 @@ public class OverridingClassLoader extends DecoratingClassLoader { } - @Nullable - private final ClassLoader overrideDelegate; + private final @Nullable ClassLoader overrideDelegate; /** @@ -115,8 +115,7 @@ protected boolean isEligibleForOverriding(String className) { * @return the Class object, or {@code null} if no class defined for that name * @throws ClassNotFoundException if the class for the given name couldn't be loaded */ - @Nullable - protected Class loadClassForOverriding(String name) throws ClassNotFoundException { + protected @Nullable Class loadClassForOverriding(String name) throws ClassNotFoundException { Class result = findLoadedClass(name); if (result == null) { byte[] bytes = loadBytesForClass(name); @@ -137,8 +136,7 @@ protected Class loadClassForOverriding(String name) throws ClassNotFoundExcep * or {@code null} if no class defined for that name * @throws ClassNotFoundException if the class for the given name couldn't be loaded */ - @Nullable - protected byte[] loadBytesForClass(String name) throws ClassNotFoundException { + protected byte @Nullable [] loadBytesForClass(String name) throws ClassNotFoundException { InputStream is = openStreamForClass(name); if (is == null) { return null; @@ -161,8 +159,7 @@ protected byte[] loadBytesForClass(String name) throws ClassNotFoundException { * @param name the name of the class * @return the InputStream containing the byte code for the specified class */ - @Nullable - protected InputStream openStreamForClass(String name) { + protected @Nullable InputStream openStreamForClass(String name) { String internalName = name.replace('.', '/') + CLASS_FILE_SUFFIX; return getParent().getResourceAsStream(internalName); } diff --git a/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java index f60d28b637ee..460327fd1f30 100644 --- a/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/ParameterNameDiscoverer.java @@ -19,7 +19,7 @@ import java.lang.reflect.Constructor; import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to discover parameter names for methods and constructors. @@ -45,8 +45,7 @@ public interface ParameterNameDiscoverer { * @return an array of parameter names if the names can be resolved, * or {@code null} if they cannot */ - @Nullable - String[] getParameterNames(Method method); + @Nullable String @Nullable [] getParameterNames(Method method); /** * Return parameter names for a constructor, or {@code null} if they cannot be determined. @@ -57,7 +56,6 @@ public interface ParameterNameDiscoverer { * @return an array of parameter names if the names can be resolved, * or {@code null} if they cannot */ - @Nullable - String[] getParameterNames(Constructor ctor); + @Nullable String @Nullable [] getParameterNames(Constructor ctor); } diff --git a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java index d9c1361d482e..5bd2683e36aa 100644 --- a/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java +++ b/spring-core/src/main/java/org/springframework/core/ParameterizedTypeReference.java @@ -19,7 +19,8 @@ import java.lang.reflect.ParameterizedType; import java.lang.reflect.Type; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java index bb6932783381..02eea42306a9 100644 --- a/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/PrioritizedParameterNameDiscoverer.java @@ -21,7 +21,7 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link ParameterNameDiscoverer} implementation that tries several discoverer @@ -49,10 +49,9 @@ public void addDiscoverer(ParameterNameDiscoverer pnd) { @Override - @Nullable - public String[] getParameterNames(Method method) { + public @Nullable String @Nullable [] getParameterNames(Method method) { for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { - String[] result = pnd.getParameterNames(method); + @Nullable String[] result = pnd.getParameterNames(method); if (result != null) { return result; } @@ -61,10 +60,9 @@ public String[] getParameterNames(Method method) { } @Override - @Nullable - public String[] getParameterNames(Constructor ctor) { + public @Nullable String @Nullable [] getParameterNames(Constructor ctor) { for (ParameterNameDiscoverer pnd : this.parameterNameDiscoverers) { - String[] result = pnd.getParameterNames(ctor); + @Nullable String[] result = pnd.getParameterNames(ctor); if (result != null) { return result; } diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java index 0356f47f59ef..ee06e4e57b5c 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapter.java @@ -18,9 +18,9 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java index 01ffdb9e209f..f816cd27d4a3 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveAdapterRegistry.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,12 +19,16 @@ import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; +import java.util.Objects; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.concurrent.Flow; import java.util.function.Function; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import org.jspecify.annotations.Nullable; import org.reactivestreams.FlowAdapters; import org.reactivestreams.Publisher; import reactor.adapter.JdkFlowAdapter; @@ -33,7 +37,6 @@ import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; @@ -56,8 +59,7 @@ */ public class ReactiveAdapterRegistry { - @Nullable - private static volatile ReactiveAdapterRegistry sharedInstance; + private static volatile @Nullable ReactiveAdapterRegistry sharedInstance; private static final boolean reactiveStreamsPresent; @@ -174,8 +176,7 @@ public boolean hasAdapters() { * Get the adapter for the given reactive type. * @return the corresponding adapter, or {@code null} if none available */ - @Nullable - public ReactiveAdapter getAdapter(Class reactiveType) { + public @Nullable ReactiveAdapter getAdapter(Class reactiveType) { return getAdapter(reactiveType, null); } @@ -188,8 +189,7 @@ public ReactiveAdapter getAdapter(Class reactiveType) { * (i.e. to adapt from; may be {@code null} if the reactive type is specified) * @return the corresponding adapter, or {@code null} if none available */ - @Nullable - public ReactiveAdapter getAdapter(@Nullable Class reactiveType, @Nullable Object source) { + public @Nullable ReactiveAdapter getAdapter(@Nullable Class reactiveType, @Nullable Object source) { if (this.adapters.isEmpty()) { return null; } @@ -383,13 +383,13 @@ void registerAdapters(ReactiveAdapterRegistry registry) { io.smallrye.mutiny.groups.MultiCreate.class, "publisher", Flow.Publisher.class); registry.registerReactiveType(uniDesc, uni -> FlowAdapters.toPublisher((Flow.Publisher) - ReflectionUtils.invokeMethod(uniToPublisher, ((io.smallrye.mutiny.Uni) uni).convert())), - publisher -> ReflectionUtils.invokeMethod(uniPublisher, io.smallrye.mutiny.Uni.createFrom(), - FlowAdapters.toFlowPublisher(publisher))); + Objects.requireNonNull(ReflectionUtils.invokeMethod(uniToPublisher, ((Uni) uni).convert()))), + publisher -> Objects.requireNonNull(ReflectionUtils.invokeMethod(uniPublisher, Uni.createFrom(), + FlowAdapters.toFlowPublisher(publisher)))); registry.registerReactiveType(multiDesc, multi -> FlowAdapters.toPublisher((Flow.Publisher) multi), - publisher -> ReflectionUtils.invokeMethod(multiPublisher, io.smallrye.mutiny.Multi.createFrom(), - FlowAdapters.toFlowPublisher(publisher))); + publisher -> Objects.requireNonNull(ReflectionUtils.invokeMethod(multiPublisher, Multi.createFrom(), + FlowAdapters.toFlowPublisher(publisher)))); } else { // Mutiny 1 based on Reactive Streams diff --git a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java index 9d53e023bd30..518ef6425083 100644 --- a/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/ReactiveTypeDescriptor.java @@ -18,7 +18,8 @@ import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -36,8 +37,7 @@ public final class ReactiveTypeDescriptor { private final boolean noValue; - @Nullable - private final Supplier emptySupplier; + private final @Nullable Supplier emptySupplier; private final boolean deferred; diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableType.java b/spring-core/src/main/java/org/springframework/core/ResolvableType.java index 03209325493a..30e69398fa76 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableType.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableType.java @@ -34,10 +34,11 @@ import java.util.Set; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.core.SerializableTypeWrapper.FieldTypeProvider; import org.springframework.core.SerializableTypeWrapper.MethodParameterTypeProvider; import org.springframework.core.SerializableTypeWrapper.TypeProvider; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -106,38 +107,29 @@ public class ResolvableType implements Serializable { /** * The component type for an array or {@code null} if the type should be deduced. */ - @Nullable - private final ResolvableType componentType; + private final @Nullable ResolvableType componentType; /** * Optional provider for the type. */ - @Nullable - private final TypeProvider typeProvider; + private final @Nullable TypeProvider typeProvider; /** * The {@code VariableResolver} to use or {@code null} if no resolver is available. */ - @Nullable - private final VariableResolver variableResolver; + private final @Nullable VariableResolver variableResolver; - @Nullable - private final Integer hash; + private final @Nullable Integer hash; - @Nullable - private Class resolved; + private @Nullable Class resolved; - @Nullable - private volatile ResolvableType superType; + private volatile @Nullable ResolvableType superType; - @Nullable - private volatile ResolvableType[] interfaces; + private volatile ResolvableType @Nullable [] interfaces; - @Nullable - private volatile ResolvableType[] generics; + private volatile ResolvableType @Nullable [] generics; - @Nullable - private volatile Boolean unresolvableGenerics; + private volatile @Nullable Boolean unresolvableGenerics; /** @@ -212,8 +204,7 @@ public Type getType() { * Return the underlying Java {@link Class} being managed, if available; * otherwise {@code null}. */ - @Nullable - public Class getRawClass() { + public @Nullable Class getRawClass() { if (this.type == this.resolved) { return this.resolved; } @@ -759,7 +750,7 @@ public ResolvableType getNested(int nestingLevel, @Nullable Map[] resolveGenerics() { + public @Nullable Class[] resolveGenerics() { ResolvableType[] generics = getGenerics(); - Class[] resolvedGenerics = new Class[generics.length]; + @Nullable Class[] resolvedGenerics = new Class[generics.length]; for (int i = 0; i < generics.length; i++) { resolvedGenerics[i] = generics[i].resolve(); } @@ -870,8 +861,7 @@ public Class[] resolveGenerics(Class fallback) { * @see #getGeneric(int...) * @see #resolve() */ - @Nullable - public Class resolveGeneric(int... indexes) { + public @Nullable Class resolveGeneric(int... indexes) { return getGeneric(indexes).resolve(); } @@ -888,8 +878,7 @@ public Class resolveGeneric(int... indexes) { * @see #resolveGeneric(int...) * @see #resolveGenerics() */ - @Nullable - public Class resolve() { + public @Nullable Class resolve() { return this.resolved; } @@ -908,8 +897,7 @@ public Class resolve(Class fallback) { return (this.resolved != null ? this.resolved : fallback); } - @Nullable - private Class resolveClass() { + private @Nullable Class resolveClass() { if (this.type == EmptyType.INSTANCE) { return null; } @@ -953,8 +941,7 @@ ResolvableType resolveType() { return NONE; } - @Nullable - private ResolvableType resolveVariable(TypeVariable variable) { + private @Nullable ResolvableType resolveVariable(TypeVariable variable) { if (this.type instanceof TypeVariable) { return resolveType().resolveVariable(variable); } @@ -1054,8 +1041,7 @@ private int calculateHashCode() { /** * Adapts this {@code ResolvableType} to a {@link VariableResolver}. */ - @Nullable - VariableResolver asVariableResolver() { + @Nullable VariableResolver asVariableResolver() { if (this == NONE) { return null; } @@ -1182,7 +1168,7 @@ public static ResolvableType forClassWithGenerics(Class clazz, Class... ge * @return a {@code ResolvableType} for the specific class and generics * @see #forClassWithGenerics(Class, Class...) */ - public static ResolvableType forClassWithGenerics(Class clazz, @Nullable ResolvableType... generics) { + public static ResolvableType forClassWithGenerics(Class clazz, @Nullable ResolvableType @Nullable ... generics) { Assert.notNull(clazz, "Class must not be null"); TypeVariable[] variables = clazz.getTypeParameters(); if (generics != null) { @@ -1465,8 +1451,7 @@ static ResolvableType forVariableBounds(TypeVariable typeVariable) { return forType(resolveBounds(typeVariable.getBounds())); } - @Nullable - private static Type resolveBounds(Type[] bounds) { + private static @Nullable Type resolveBounds(Type[] bounds) { if (bounds.length == 0 || bounds[0] == Object.class) { return null; } @@ -1587,8 +1572,7 @@ interface VariableResolver extends Serializable { * @param variable the variable to resolve * @return the resolved variable, or {@code null} if not found */ - @Nullable - ResolvableType resolveVariable(TypeVariable variable); + @Nullable ResolvableType resolveVariable(TypeVariable variable); } @@ -1602,8 +1586,7 @@ private static class DefaultVariableResolver implements VariableResolver { } @Override - @Nullable - public ResolvableType resolveVariable(TypeVariable variable) { + public @Nullable ResolvableType resolveVariable(TypeVariable variable) { return this.source.resolveVariable(variable); } @@ -1619,16 +1602,15 @@ private static class TypeVariablesVariableResolver implements VariableResolver { private final TypeVariable[] variables; - private final ResolvableType[] generics; + private final @Nullable ResolvableType[] generics; - public TypeVariablesVariableResolver(TypeVariable[] variables, ResolvableType[] generics) { + public TypeVariablesVariableResolver(TypeVariable[] variables, @Nullable ResolvableType[] generics) { this.variables = variables; this.generics = generics; } @Override - @Nullable - public ResolvableType resolveVariable(TypeVariable variable) { + public @Nullable ResolvableType resolveVariable(TypeVariable variable) { TypeVariable variableToCompare = SerializableTypeWrapper.unwrap(variable); for (int i = 0; i < this.variables.length; i++) { TypeVariable resolvedVariable = SerializableTypeWrapper.unwrap(this.variables[i]); @@ -1671,8 +1653,7 @@ public String getTypeName() { } @Override - @Nullable - public Type getOwnerType() { + public @Nullable Type getOwnerType() { return null; } @@ -1821,8 +1802,7 @@ public ResolvableType[] getBounds() { * @param type the source type * @return a {@link WildcardBounds} instance or {@code null} */ - @Nullable - public static WildcardBounds get(ResolvableType type) { + public static @Nullable WildcardBounds get(ResolvableType type) { ResolvableType candidate = type; while (!(candidate.getType() instanceof WildcardType || candidate.isUnresolvableTypeVariable())) { if (candidate == NONE) { diff --git a/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java b/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java index 0f5b6b585a4b..037bf3d8cca7 100644 --- a/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java +++ b/spring-core/src/main/java/org/springframework/core/ResolvableTypeProvider.java @@ -16,7 +16,7 @@ package org.springframework.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Any object can implement this interface to provide its actual {@link ResolvableType}. @@ -37,7 +37,6 @@ public interface ResolvableTypeProvider { * Return the {@link ResolvableType} describing this instance * (or {@code null} if some sort of default should be applied instead). */ - @Nullable - ResolvableType getResolvableType(); + @Nullable ResolvableType getResolvableType(); } diff --git a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java index ba7bc2a60f22..508081bddc24 100644 --- a/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.java +++ b/spring-core/src/main/java/org/springframework/core/SerializableTypeWrapper.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,7 +29,8 @@ import java.lang.reflect.TypeVariable; import java.lang.reflect.WildcardType; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -68,8 +69,7 @@ private SerializableTypeWrapper() { /** * Return a {@link Serializable} variant of {@link Field#getGenericType()}. */ - @Nullable - public static Type forField(Field field) { + public static @Nullable Type forField(Field field) { return forTypeProvider(new FieldTypeProvider(field)); } @@ -77,8 +77,7 @@ public static Type forField(Field field) { * Return a {@link Serializable} variant of * {@link MethodParameter#getGenericParameterType()}. */ - @Nullable - public static Type forMethodParameter(MethodParameter methodParameter) { + public static @Nullable Type forMethodParameter(MethodParameter methodParameter) { return forTypeProvider(new MethodParameterTypeProvider(methodParameter)); } @@ -101,8 +100,7 @@ public static T unwrap(T type) { *

If type artifacts are generally not serializable in the current runtime * environment, this delegate will simply return the original {@code Type} as-is. */ - @Nullable - static Type forTypeProvider(TypeProvider provider) { + static @Nullable Type forTypeProvider(TypeProvider provider) { Type providedType = provider.getType(); if (providedType == null || providedType instanceof Serializable) { // No serializable type wrapping necessary (for example, for java.lang.Class) @@ -154,15 +152,13 @@ interface TypeProvider extends Serializable { /** * Return the (possibly non {@link Serializable}) {@link Type}. */ - @Nullable - Type getType(); + @Nullable Type getType(); /** * Return the source of the type, or {@code null} if not known. *

The default implementation returns {@code null}. */ - @Nullable - default Object getSource() { + default @Nullable Object getSource() { return null; } } @@ -183,8 +179,7 @@ public TypeProxyInvocationHandler(TypeProvider provider) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) { case "equals" -> { Object other = args[0]; @@ -210,7 +205,7 @@ else if (Type[].class == method.getReturnType() && ObjectUtils.isEmpty(args)) { if (returnValue == null) { return null; } - Type[] result = new Type[((Type[]) returnValue).length]; + @Nullable Type[] result = new Type[((Type[]) returnValue).length]; for (int i = 0; i < result.length; i++) { result[i] = forTypeProvider(new MethodInvokeTypeProvider(this.provider, method, i)); } @@ -273,8 +268,7 @@ private void readObject(ObjectInputStream inputStream) throws IOException, Class @SuppressWarnings("serial") static class MethodParameterTypeProvider implements TypeProvider { - @Nullable - private final String methodName; + private final @Nullable String methodName; private final Class[] parameterTypes; @@ -337,8 +331,7 @@ static class MethodInvokeTypeProvider implements TypeProvider { private transient Method method; - @Nullable - private transient volatile Object result; + private transient volatile @Nullable Object result; public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) { this.provider = provider; @@ -349,8 +342,7 @@ public MethodInvokeTypeProvider(TypeProvider provider, Method method, int index) } @Override - @Nullable - public Type getType() { + public @Nullable Type getType() { Object result = this.result; if (result == null) { // Lazy invocation of the target method on the provided type @@ -362,8 +354,7 @@ public Type getType() { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java b/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java index 695d09c68e9b..1a8162d277a3 100644 --- a/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java +++ b/spring-core/src/main/java/org/springframework/core/SmartClassLoader.java @@ -18,7 +18,7 @@ import java.security.ProtectionDomain; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to be implemented by a reloading-aware ClassLoader diff --git a/spring-core/src/main/java/org/springframework/core/SortedProperties.java b/spring-core/src/main/java/org/springframework/core/SortedProperties.java index cb3e7a93e28b..6ec7dc775ffc 100644 --- a/spring-core/src/main/java/org/springframework/core/SortedProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SortedProperties.java @@ -30,7 +30,7 @@ import java.util.Set; import java.util.TreeSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Specialization of {@link Properties} that sorts properties alphanumerically 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 299ec0d987b1..b38f6b9e6682 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringProperties.java +++ b/spring-core/src/main/java/org/springframework/core/SpringProperties.java @@ -21,24 +21,27 @@ import java.net.URL; import java.util.Properties; -import org.springframework.lang.Nullable; +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 * @see org.springframework.expression.spel.SpelParserConfiguration#SPRING_EXPRESSION_COMPILER_MODE_PROPERTY_NAME * @see org.springframework.jdbc.core.StatementCreatorUtils#IGNORE_GETPARAMETERTYPE_PROPERTY_NAME @@ -97,8 +100,7 @@ public static void setProperty(String key, @Nullable String value) { * @param key the property key * @return the associated property value, or {@code null} if none found */ - @Nullable - public static String getProperty(String key) { + public static @Nullable String getProperty(String key) { String value = localProperties.getProperty(key); if (value == null) { try { @@ -117,17 +119,42 @@ public static String getProperty(String key) { * @param key the property key */ public static void setFlag(String key) { - localProperties.put(key, Boolean.TRUE.toString()); + localProperties.setProperty(key, Boolean.TRUE.toString()); + } + + /** + * 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)); } /** * Retrieve the flag for the given property key. * @param key the property key * @return {@code true} if the property is set to the string "true" - * (ignoring case), {@code} false otherwise + * (ignoring case), {@code false} otherwise */ public static boolean getFlag(String key) { 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/SpringVersion.java b/spring-core/src/main/java/org/springframework/core/SpringVersion.java index 7d03c2160747..8d6bf5534067 100644 --- a/spring-core/src/main/java/org/springframework/core/SpringVersion.java +++ b/spring-core/src/main/java/org/springframework/core/SpringVersion.java @@ -16,7 +16,7 @@ package org.springframework.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Class that exposes the Spring version. Fetches the @@ -42,8 +42,7 @@ private SpringVersion() { * or {@code null} if it cannot be determined. * @see Package#getImplementationVersion() */ - @Nullable - public static String getVersion() { + public static @Nullable String getVersion() { Package pkg = SpringVersion.class.getPackage(); return (pkg != null ? pkg.getImplementationVersion() : null); } diff --git a/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java b/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java index 9bce47f435b4..6a8cbdf649be 100644 --- a/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java +++ b/spring-core/src/main/java/org/springframework/core/StandardReflectionParameterNameDiscoverer.java @@ -20,7 +20,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Parameter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link ParameterNameDiscoverer} implementation which uses JDK 8's reflection facilities @@ -39,19 +39,16 @@ public class StandardReflectionParameterNameDiscoverer implements ParameterNameDiscoverer { @Override - @Nullable - public String[] getParameterNames(Method method) { + public @Nullable String @Nullable [] getParameterNames(Method method) { return getParameterNames(method.getParameters()); } @Override - @Nullable - public String[] getParameterNames(Constructor ctor) { + public @Nullable String @Nullable [] getParameterNames(Constructor ctor) { return getParameterNames(ctor.getParameters()); } - @Nullable - private String[] getParameterNames(Parameter[] parameters) { + private String @Nullable [] getParameterNames(Parameter[] parameters) { String[] parameterNames = new String[parameters.length]; for (int i = 0; i < parameters.length; i++) { Parameter param = parameters[i]; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java index 978e9684c25a..0c80f267f6c8 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AbstractMergedAnnotation.java @@ -21,7 +21,8 @@ import java.util.Optional; import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -34,8 +35,7 @@ */ abstract class AbstractMergedAnnotation implements MergedAnnotation { - @Nullable - private volatile A synthesizedAnnotation; + private volatile @Nullable A synthesizedAnnotation; @Override @@ -230,8 +230,7 @@ private T getRequiredAttributeValue(String attributeName, Class type) { * @throws IllegalArgumentException if the source type is not compatible * @throws NoSuchElementException if the value is required but not found */ - @Nullable - protected abstract T getAttributeValue(String attributeName, Class type); + protected abstract @Nullable T getAttributeValue(String attributeName, Class type); /** * Factory method used to create the synthesized annotation. diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java index 2a875b04767a..48722cbe87fb 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AliasFor.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. @@ -74,7 +74,8 @@ *

    *
  1. The attribute that is an alias for an attribute in a meta-annotation * must be annotated with {@code @AliasFor}, and {@link #attribute} must - * reference the attribute in the meta-annotation.
  2. + * reference the attribute in the meta-annotation (unless both attributes have + * the same name). *
  3. Aliased attributes must declare the same return type.
  4. *
  5. {@link #annotation} must reference the meta-annotation.
  6. *
  7. The referenced meta-annotation must be meta-present on the @@ -165,10 +166,10 @@ * } * *

    Spring Annotations Supporting Attribute Aliases

    - *

    As of Spring Framework 4.2, several annotations within core Spring - * have been updated to use {@code @AliasFor} to configure their internal - * attribute aliases. Consult the Javadoc for individual annotations as well - * as the reference manual for details. + *

    Many annotations within the Spring Framework and across the Spring + * ecosystem rely on {@code @AliasFor} to configure attribute aliases. Consult + * the Javadoc for individual annotations as well as reference documentation for + * details. * * @author Sam Brannen * @since 4.2 @@ -191,6 +192,8 @@ /** * The name of the attribute that this attribute is an alias for. + *

    May be omitted if this attribute is an alias for an attribute in a + * meta-annotation and both attributes have the same name. * @see #value */ @AliasFor("value") diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementAdapter.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementAdapter.java new file mode 100644 index 000000000000..f6456d879c18 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementAdapter.java @@ -0,0 +1,130 @@ +/* + * Copyright 2002-2025 the original author 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.annotation; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; + +import org.jspecify.annotations.Nullable; + +/** + * Adapter for exposing a set of annotations as an {@link AnnotatedElement}, in + * particular as input for various methods in {@link AnnotatedElementUtils}. + * + * @author Juergen Hoeller + * @author Sam Brannen + * @since 7.0 + * @see #from(Annotation[]) + * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) + * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) + */ +@SuppressWarnings("serial") +public final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { + + private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); + + + /** + * Create an {@code AnnotatedElementAdapter} from the supplied annotations. + *

    The supplied annotations will be considered to be both present + * and directly present with regard to the results returned from + * methods such as {@link #getAnnotation(Class)}, + * {@link #getDeclaredAnnotation(Class)}, etc. + *

    If the supplied annotations array is either {@code null} or empty, this + * factory method will return an {@linkplain #isEmpty() empty} adapter. + * @param annotations the annotations to expose via the {@link AnnotatedElement} + * API + * @return a new {@code AnnotatedElementAdapter} + */ + public static AnnotatedElementAdapter from(Annotation @Nullable [] annotations) { + if (annotations == null || annotations.length == 0) { + return EMPTY; + } + return new AnnotatedElementAdapter(annotations); + } + + + private final Annotation[] annotations; + + + private AnnotatedElementAdapter(Annotation[] annotations) { + this.annotations = annotations; + } + + + @Override + public boolean isAnnotationPresent(Class annotationClass) { + for (Annotation annotation : this.annotations) { + if (annotation.annotationType() == annotationClass) { + return true; + } + } + return false; + } + + @Override + public @Nullable A getAnnotation(Class annotationClass) { + for (Annotation annotation : this.annotations) { + if (annotation.annotationType() == annotationClass) { + return annotationClass.cast(annotation); + } + } + return null; + } + + @Override + public Annotation[] getAnnotations() { + return (isEmpty() ? this.annotations : this.annotations.clone()); + } + + @Override + public @Nullable A getDeclaredAnnotation(Class annotationClass) { + return getAnnotation(annotationClass); + } + + @Override + public Annotation[] getDeclaredAnnotations() { + return getAnnotations(); + } + + /** + * Determine if this {@code AnnotatedElementAdapter} is empty. + * @return {@code true} if this adapter contains no annotations + */ + public boolean isEmpty() { + return (this == EMPTY); + } + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof AnnotatedElementAdapter that && + Arrays.equals(this.annotations, that.annotations))); + } + + @Override + public int hashCode() { + return Arrays.hashCode(this.annotations); + } + + @Override + public String toString() { + return Arrays.toString(this.annotations); + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java index 545b8a098ed2..13407873bf18 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedElementUtils.java @@ -24,10 +24,11 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.core.BridgeMethodResolver; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; /** @@ -35,13 +36,13 @@ * repeatable annotations on {@link AnnotatedElement AnnotatedElements}. * *

    {@code AnnotatedElementUtils} defines the public API for Spring's - * meta-annotation programming model with support for annotation attribute - * overrides and {@link AliasFor @AliasFor}. Note, however, that - * {@code AnnotatedElementUtils} is effectively a facade for the - * {@link MergedAnnotations} API. For fine-grained support consider using the + * meta-annotation programming model with support for attribute aliases and + * annotation attribute overrides configured via {@link AliasFor @AliasFor}. + * Note, however, that {@code AnnotatedElementUtils} is effectively a facade for + * the {@link MergedAnnotations} API. For fine-grained support consider using the * {@code MergedAnnotations} API directly. If you do not need support for - * annotation attribute overrides, {@code @AliasFor}, or merged annotations, - * consider using {@link AnnotationUtils} instead. + * {@code @AliasFor} or merged annotations, consider using {@link AnnotationUtils} + * instead. * *

    Note that the features of this class are not provided by the JDK's * introspection facilities themselves. @@ -99,12 +100,12 @@ public abstract class AnnotatedElementUtils { /** * Build an adapted {@link AnnotatedElement} for the given annotations, - * typically for use with other methods on {@link AnnotatedElementUtils}. + * typically for use with other methods in {@link AnnotatedElementUtils}. * @param annotations the annotations to expose through the {@code AnnotatedElement} * @since 4.3 */ public static AnnotatedElement forAnnotations(Annotation... annotations) { - return new AnnotatedElementForAnnotations(annotations); + return AnnotatedElementAdapter.from(annotations); } /** @@ -236,8 +237,8 @@ public static boolean isAnnotated(AnnotatedElement element, String annotationNam * the annotation hierarchy above the supplied {@code element} and * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method delegates to {@link #getMergedAnnotationAttributes(AnnotatedElement, String)}. * @param element the annotated element * @param annotationType the annotation type to find @@ -248,8 +249,7 @@ public static boolean isAnnotated(AnnotatedElement element, String annotationNam * @see #getMergedAnnotation(AnnotatedElement, Class) * @see #findMergedAnnotation(AnnotatedElement, Class) */ - @Nullable - public static AnnotationAttributes getMergedAnnotationAttributes( + public static @Nullable AnnotationAttributes getMergedAnnotationAttributes( AnnotatedElement element, Class annotationType) { MergedAnnotation mergedAnnotation = getAnnotations(element) @@ -262,8 +262,8 @@ public static AnnotationAttributes getMergedAnnotationAttributes( * the annotation hierarchy above the supplied {@code element} and * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method delegates to {@link #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean)}, * supplying {@code false} for {@code classValuesAsString} and {@code nestedAnnotationsAsMap}. * @param element the annotated element @@ -275,8 +275,7 @@ public static AnnotationAttributes getMergedAnnotationAttributes( * @see #findMergedAnnotation(AnnotatedElement, Class) * @see #getAllAnnotationAttributes(AnnotatedElement, String) */ - @Nullable - public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, + public static @Nullable AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, String annotationName) { return getMergedAnnotationAttributes(element, annotationName, false, false); @@ -287,9 +286,8 @@ public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElemen * the annotation hierarchy above the supplied {@code element} and * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. - *

    Attributes from lower levels in the annotation hierarchy override attributes - * of the same name from higher levels, and {@link AliasFor @AliasFor} semantics are - * fully supported, both within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    In contrast to {@link #getAllAnnotationAttributes}, the search algorithm used by * this method will stop searching the annotation hierarchy once the first annotation * of the specified {@code annotationName} has been found. As a consequence, @@ -308,8 +306,7 @@ public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElemen * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) */ - @Nullable - public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, + public static @Nullable AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElement element, String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { MergedAnnotation mergedAnnotation = getAnnotations(element) @@ -323,16 +320,15 @@ public static AnnotationAttributes getMergedAnnotationAttributes(AnnotatedElemen * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy, and synthesize * the result back into an annotation of the specified {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. * @param element the annotated element * @param annotationType the annotation type to find * @return the merged, synthesized {@code Annotation}, or {@code null} if not found * @since 4.2 * @see #findMergedAnnotation(AnnotatedElement, Class) */ - @Nullable - public static A getMergedAnnotation(AnnotatedElement element, Class annotationType) { + public static @Nullable A getMergedAnnotation(AnnotatedElement element, Class annotationType) { // Shortcut: directly present on the element, with no merging needed? if (AnnotationFilter.PLAIN.matches(annotationType) || AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) { @@ -351,8 +347,8 @@ public static A getMergedAnnotation(AnnotatedElement elem * matching attributes from annotations in lower levels of the annotation * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows get semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element (never {@code null}) @@ -378,8 +374,8 @@ public static Set getAllMergedAnnotations( * matching attributes from annotations in lower levels of the * annotation hierarchy and synthesize the results back into an annotation * of the corresponding {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows get semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element (never {@code null}) @@ -405,9 +401,9 @@ public static Set getAllMergedAnnotations(AnnotatedElement element, * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. *

    The container type that holds the repeatable annotations will be looked up - * via {@link java.lang.annotation.Repeatable}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + * via {@link java.lang.annotation.Repeatable @Repeatable}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows get semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element (never {@code null}) @@ -434,8 +430,8 @@ public static Set getMergedRepeatableAnnotations( * matching attributes from annotations in lower levels of the annotation * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows get semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. *

    WARNING: if the supplied {@code containerType} is not @@ -446,13 +442,17 @@ public static Set getMergedRepeatableAnnotations( * support such a use case, favor {@link #getMergedRepeatableAnnotations(AnnotatedElement, Class)} * over this method or alternatively use the {@link MergedAnnotations} API * directly in conjunction with {@link RepeatableContainers} that are - * {@linkplain RepeatableContainers#and(Class, Class) composed} to support - * multiple repeatable annotation types. + * {@linkplain RepeatableContainers#plus(Class, Class) composed} to support + * multiple repeatable annotation types — for example: + *

    +	 * RepeatableContainers.standardRepeatables()
    +	 *     .plus(MyRepeatable1.class, MyContainer1.class)
    +	 *     .plus(MyRepeatable2.class, MyContainer2.class);
    * @param element the annotated element (never {@code null}) - * @param annotationType the annotation type to find (never {@code null}) - * @param containerType the type of the container that holds the annotations; - * may be {@code null} if the container type should be looked up via - * {@link java.lang.annotation.Repeatable} + * @param annotationType the repeatable annotation type to find (never {@code null}) + * @param containerType the type of the container that holds the repeatable + * annotations; may be {@code null} if the container type should be looked up + * via {@link java.lang.annotation.Repeatable @Repeatable} * @return the set of all merged repeatable {@code Annotations} found, * or an empty set if none were found * @throws IllegalArgumentException if the {@code element} or {@code annotationType} @@ -467,7 +467,7 @@ public static Set getMergedRepeatableAnnotations( AnnotatedElement element, Class annotationType, @Nullable Class containerType) { - return getRepeatableAnnotations(element, containerType, annotationType) + return getRepeatableAnnotations(element, annotationType, containerType) .stream(annotationType) .collect(MergedAnnotationCollectors.toAnnotationSet()); } @@ -486,8 +486,7 @@ public static Set getMergedRepeatableAnnotations( * attributes from all annotations found, or {@code null} if not found * @see #getAllAnnotationAttributes(AnnotatedElement, String, boolean, boolean) */ - @Nullable - public static MultiValueMap getAllAnnotationAttributes( + public static @Nullable MultiValueMap getAllAnnotationAttributes( AnnotatedElement element, String annotationName) { return getAllAnnotationAttributes(element, annotationName, false, false); @@ -511,8 +510,7 @@ public static MultiValueMap getAllAnnotationAttributes( * @return a {@link MultiValueMap} keyed by attribute name, containing the annotation * attributes from all annotations found, or {@code null} if not found */ - @Nullable - public static MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, + public static @Nullable MultiValueMap getAllAnnotationAttributes(AnnotatedElement element, String annotationName, final boolean classValuesAsString, final boolean nestedAnnotationsAsMap) { Adapt[] adaptations = Adapt.values(classValuesAsString, nestedAnnotationsAsMap); @@ -551,10 +549,8 @@ public static boolean hasAnnotation(AnnotatedElement element, Classabove the supplied {@code element} and * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. - *

    Attributes from lower levels in the annotation hierarchy override - * attributes of the same name from higher levels, and - * {@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    In contrast to {@link #getAllAnnotationAttributes}, the search algorithm * used by this method will stop searching the annotation hierarchy once the * first annotation of the specified {@code annotationType} has been found. @@ -573,8 +569,7 @@ public static boolean hasAnnotation(AnnotatedElement element, Class annotationType, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { MergedAnnotation mergedAnnotation = findAnnotations(element) @@ -587,10 +582,8 @@ public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedEleme * the annotation hierarchy above the supplied {@code element} and * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy. - *

    Attributes from lower levels in the annotation hierarchy override - * attributes of the same name from higher levels, and - * {@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    In contrast to {@link #getAllAnnotationAttributes}, the search * algorithm used by this method will stop searching the annotation * hierarchy once the first annotation of the specified @@ -609,8 +602,7 @@ public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedEleme * @see #findMergedAnnotation(AnnotatedElement, Class) * @see #getMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) */ - @Nullable - public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, + public static @Nullable AnnotationAttributes findMergedAnnotationAttributes(AnnotatedElement element, String annotationName, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { MergedAnnotation mergedAnnotation = findAnnotations(element) @@ -624,8 +616,8 @@ public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedEleme * merge that annotation's attributes with matching attributes from * annotations in lower levels of the annotation hierarchy, and synthesize * the result back into an annotation of the specified {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both - * within a single annotation and within the annotation hierarchy. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element @@ -636,8 +628,7 @@ public static AnnotationAttributes findMergedAnnotationAttributes(AnnotatedEleme * @see #findMergedAnnotationAttributes(AnnotatedElement, String, boolean, boolean) * @see #getMergedAnnotationAttributes(AnnotatedElement, Class) */ - @Nullable - public static A findMergedAnnotation(AnnotatedElement element, Class annotationType) { + public static @Nullable A findMergedAnnotation(AnnotatedElement element, Class annotationType) { // Shortcut: directly present on the element, with no merging needed? if (AnnotationFilter.PLAIN.matches(annotationType) || AnnotationsScanner.hasPlainJavaAnnotationsOnly(element)) { @@ -656,8 +647,8 @@ public static A findMergedAnnotation(AnnotatedElement ele * matching attributes from annotations in lower levels of the annotation * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element (never {@code null}) @@ -681,8 +672,8 @@ public static Set findAllMergedAnnotations(AnnotatedEl * matching attributes from annotations in lower levels of the * annotation hierarchy and synthesize the results back into an annotation * of the corresponding {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element (never {@code null}) @@ -707,9 +698,9 @@ public static Set findAllMergedAnnotations(AnnotatedElement element, * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. *

    The container type that holds the repeatable annotations will be looked up - * via {@link java.lang.annotation.Repeatable}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + * via {@link java.lang.annotation.Repeatable @Repeatable}. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. * @param element the annotated element (never {@code null}) @@ -736,8 +727,8 @@ public static Set findMergedRepeatableAnnotations(Anno * matching attributes from annotations in lower levels of the annotation * hierarchy and synthesize the results back into an annotation of the specified * {@code annotationType}. - *

    {@link AliasFor @AliasFor} semantics are fully supported, both within a - * single annotation and within annotation hierarchies. + *

    {@link AliasFor @AliasFor} semantics are fully supported, both within + * a single annotation and within the annotation hierarchy. *

    This method follows find semantics as described in the * {@linkplain AnnotatedElementUtils class-level javadoc}. *

    WARNING: if the supplied {@code containerType} is not @@ -748,13 +739,17 @@ public static Set findMergedRepeatableAnnotations(Anno * support such a use case, favor {@link #findMergedRepeatableAnnotations(AnnotatedElement, Class)} * over this method or alternatively use the {@link MergedAnnotations} API * directly in conjunction with {@link RepeatableContainers} that are - * {@linkplain RepeatableContainers#and(Class, Class) composed} to support - * multiple repeatable annotation types. + * {@linkplain RepeatableContainers#plus(Class, Class) composed} to support + * multiple repeatable annotation types — for example: + *

    +	 * RepeatableContainers.standardRepeatables()
    +	 *     .plus(MyRepeatable1.class, MyContainer1.class)
    +	 *     .plus(MyRepeatable2.class, MyContainer2.class);
    * @param element the annotated element (never {@code null}) - * @param annotationType the annotation type to find (never {@code null}) - * @param containerType the type of the container that holds the annotations; - * may be {@code null} if the container type should be looked up via - * {@link java.lang.annotation.Repeatable} + * @param annotationType the repeatable annotation type to find (never {@code null}) + * @param containerType the type of the container that holds the repeatable + * annotations; may be {@code null} if the container type should be looked up + * via {@link java.lang.annotation.Repeatable @Repeatable} * @return the set of all merged repeatable {@code Annotations} found, * or an empty set if none were found * @throws IllegalArgumentException if the {@code element} or {@code annotationType} @@ -768,7 +763,7 @@ public static Set findMergedRepeatableAnnotations(Anno public static Set findMergedRepeatableAnnotations(AnnotatedElement element, Class annotationType, @Nullable Class containerType) { - return findRepeatableAnnotations(element, containerType, annotationType) + return findRepeatableAnnotations(element, annotationType, containerType) .stream(annotationType) .sorted(highAggregateIndexesFirst()) .collect(MergedAnnotationCollectors.toAnnotationSet()); @@ -779,11 +774,11 @@ private static MergedAnnotations getAnnotations(AnnotatedElement element) { } private static MergedAnnotations getRepeatableAnnotations(AnnotatedElement element, - @Nullable Class containerType, Class annotationType) { + Class annotationType, @Nullable Class containerType) { RepeatableContainers repeatableContainers; if (containerType == null) { - // Invoke RepeatableContainers.of() in order to adhere to the contract of + // Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of // getMergedRepeatableAnnotations() which states that an IllegalArgumentException // will be thrown if the container cannot be resolved. // @@ -792,11 +787,11 @@ private static MergedAnnotations getRepeatableAnnotations(AnnotatedElement eleme // annotation types). // // See https://github.com/spring-projects/spring-framework/issues/20279 - RepeatableContainers.of(annotationType, null); + RepeatableContainers.explicitRepeatable(annotationType, null); repeatableContainers = RepeatableContainers.standardRepeatables(); } else { - repeatableContainers = RepeatableContainers.of(annotationType, containerType); + repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType); } return MergedAnnotations.from(element, SearchStrategy.INHERITED_ANNOTATIONS, repeatableContainers); } @@ -806,11 +801,11 @@ private static MergedAnnotations findAnnotations(AnnotatedElement element) { } private static MergedAnnotations findRepeatableAnnotations(AnnotatedElement element, - @Nullable Class containerType, Class annotationType) { + Class annotationType, @Nullable Class containerType) { RepeatableContainers repeatableContainers; if (containerType == null) { - // Invoke RepeatableContainers.of() in order to adhere to the contract of + // Invoke RepeatableContainers.explicitRepeatable() in order to adhere to the contract of // findMergedRepeatableAnnotations() which states that an IllegalArgumentException // will be thrown if the container cannot be resolved. // @@ -819,17 +814,16 @@ private static MergedAnnotations findRepeatableAnnotations(AnnotatedElement elem // annotation types). // // See https://github.com/spring-projects/spring-framework/issues/20279 - RepeatableContainers.of(annotationType, null); + RepeatableContainers.explicitRepeatable(annotationType, null); repeatableContainers = RepeatableContainers.standardRepeatables(); } else { - repeatableContainers = RepeatableContainers.of(annotationType, containerType); + repeatableContainers = RepeatableContainers.explicitRepeatable(annotationType, containerType); } return MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY, repeatableContainers); } - @Nullable - private static MultiValueMap nullIfEmpty(MultiValueMap map) { + private static @Nullable MultiValueMap nullIfEmpty(MultiValueMap map) { return (map.isEmpty() ? null : map); } @@ -837,8 +831,7 @@ private static Comparator> highAggreg return Comparator.> comparingInt(MergedAnnotation::getAggregateIndex).reversed(); } - @Nullable - private static AnnotationAttributes getAnnotationAttributes(MergedAnnotation annotation, + private static @Nullable AnnotationAttributes getAnnotationAttributes(MergedAnnotation annotation, boolean classValuesAsString, boolean nestedAnnotationsAsMap) { if (!annotation.isPresent()) { @@ -847,40 +840,4 @@ private static AnnotationAttributes getAnnotationAttributes(MergedAnnotation return annotation.asAnnotationAttributes(Adapt.values(classValuesAsString, nestedAnnotationsAsMap)); } - - /** - * Adapted {@link AnnotatedElement} that holds specific annotations. - */ - private static class AnnotatedElementForAnnotations implements AnnotatedElement { - - private final Annotation[] annotations; - - AnnotatedElementForAnnotations(Annotation... annotations) { - this.annotations = annotations; - } - - @Override - @SuppressWarnings("unchecked") - @Nullable - public T getAnnotation(Class annotationClass) { - for (Annotation annotation : this.annotations) { - if (annotation.annotationType() == annotationClass) { - return (T) annotation; - } - } - return null; - } - - @Override - public Annotation[] getAnnotations() { - return this.annotations.clone(); - } - - @Override - public Annotation[] getDeclaredAnnotations() { - return this.annotations.clone(); - } - - } - } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java index fd8fb1079978..5335da3ccd12 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotatedMethod.java @@ -22,11 +22,11 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.BridgeMethodResolver; import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -52,8 +52,7 @@ public class AnnotatedMethod { private final MethodParameter[] parameters; - @Nullable - private volatile List inheritedParameterAnnotations; + private volatile @Nullable List inheritedParameterAnnotations; /** @@ -148,8 +147,7 @@ public boolean isVoid() { * @return the annotation, or {@code null} if none found * @see AnnotatedElementUtils#findMergedAnnotation */ - @Nullable - public A getMethodAnnotation(Class annotationType) { + public @Nullable A getMethodAnnotation(Class annotationType) { return AnnotatedElementUtils.findMergedAnnotation(this.method, annotationType); } @@ -231,8 +229,7 @@ public String toString() { // Support methods for use in subclass variants - @Nullable - protected static Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { + protected static @Nullable Object findProvidedArgument(MethodParameter parameter, @Nullable Object... providedArgs) { if (!ObjectUtils.isEmpty(providedArgs)) { for (Object providedArg : providedArgs) { if (parameter.getParameterType().isInstance(providedArg)) { @@ -254,8 +251,7 @@ protected static String formatArgumentError(MethodParameter param, String messag */ protected class AnnotatedMethodParameter extends SynthesizingMethodParameter { - @Nullable - private volatile Annotation[] combinedAnnotations; + private volatile Annotation @Nullable [] combinedAnnotations; public AnnotatedMethodParameter(int index) { super(AnnotatedMethod.this.getBridgedMethod(), index); @@ -267,7 +263,6 @@ protected AnnotatedMethodParameter(AnnotatedMethodParameter original) { } @Override - @NonNull public Method getMethod() { return AnnotatedMethod.this.getBridgedMethod(); } @@ -278,8 +273,7 @@ public Class getContainingClass() { } @Override - @Nullable - public T getMethodAnnotation(Class annotationType) { + public @Nullable T getMethodAnnotation(Class annotationType) { return AnnotatedMethod.this.getMethodAnnotation(annotationType); } @@ -335,8 +329,7 @@ public AnnotatedMethodParameter clone() { */ private class ReturnValueMethodParameter extends AnnotatedMethodParameter { - @Nullable - private final Class returnValueType; + private final @Nullable Class returnValueType; public ReturnValueMethodParameter(@Nullable Object returnValue) { super(-1); diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java index 7732e008062e..c4d8829a4baa 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAttributes.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,8 @@ import java.util.LinkedHashMap; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -44,12 +45,11 @@ * @see AnnotatedElementUtils */ @SuppressWarnings("serial") -public class AnnotationAttributes extends LinkedHashMap { +public class AnnotationAttributes extends LinkedHashMap { private static final String UNKNOWN = "unknown"; - @Nullable - private final Class annotationType; + private final @Nullable Class annotationType; final String displayName; @@ -83,7 +83,7 @@ public AnnotationAttributes(int initialCapacity) { * @param map original source of annotation attribute key-value pairs * @see #fromMap(Map) */ - public AnnotationAttributes(Map map) { + public AnnotationAttributes(Map map) { super(map); this.annotationType = null; this.displayName = UNKNOWN; @@ -147,8 +147,7 @@ public AnnotationAttributes(String annotationType, @Nullable ClassLoader classLo } @SuppressWarnings("unchecked") - @Nullable - private static Class getAnnotationType(String annotationType, @Nullable ClassLoader classLoader) { + private static @Nullable Class getAnnotationType(String annotationType, @Nullable ClassLoader classLoader) { if (classLoader != null) { try { return (Class) classLoader.loadClass(annotationType); @@ -166,8 +165,7 @@ private static Class getAnnotationType(String annotationTy * @return the annotation type, or {@code null} if unknown * @since 4.2 */ - @Nullable - public Class annotationType() { + public @Nullable Class annotationType() { return this.annotationType; } @@ -378,10 +376,10 @@ private T getRequiredAttribute(String attributeName, Class expectedType) @Override public String toString() { - Iterator> entries = entrySet().iterator(); + Iterator> entries = entrySet().iterator(); StringBuilder sb = new StringBuilder("{"); while (entries.hasNext()) { - Map.Entry entry = entries.next(); + Map.Entry entry = entries.next(); sb.append(entry.getKey()); sb.append('='); sb.append(valueToString(entry.getValue())); @@ -393,7 +391,7 @@ public String toString() { return sb.toString(); } - private String valueToString(Object value) { + private String valueToString(@Nullable Object value) { if (value == this) { return "(this Map)"; } @@ -412,8 +410,7 @@ private String valueToString(Object value) { * to the {@link #AnnotationAttributes(Map)} constructor. * @param map original source of annotation attribute key-value pairs */ - @Nullable - public static AnnotationAttributes fromMap(@Nullable Map map) { + public static @Nullable AnnotationAttributes fromMap(@Nullable Map map) { if (map == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java index 3ac17cd27e56..ac1b86f2a46f 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationAwareOrderComparator.java @@ -20,10 +20,11 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.DecoratingProxy; import org.springframework.core.OrderComparator; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; /** * {@code AnnotationAwareOrderComparator} is an extension of @@ -59,8 +60,7 @@ public class AnnotationAwareOrderComparator extends OrderComparator { * check in the superclass. */ @Override - @Nullable - protected Integer findOrder(Object obj) { + protected @Nullable Integer findOrder(Object obj) { Integer order = super.findOrder(obj); if (order != null) { return order; @@ -68,8 +68,7 @@ protected Integer findOrder(Object obj) { return findOrderFromAnnotation(obj); } - @Nullable - private Integer findOrderFromAnnotation(Object obj) { + private @Nullable Integer findOrderFromAnnotation(Object obj) { AnnotatedElement element = (obj instanceof AnnotatedElement ae ? ae : obj.getClass()); MergedAnnotations annotations = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY); Integer order = OrderUtils.getOrderFromAnnotations(element, annotations); @@ -86,8 +85,7 @@ private Integer findOrderFromAnnotation(Object obj) { * multiple matches but only one object to be returned. */ @Override - @Nullable - public Integer getPriority(Object obj) { + public @Nullable Integer getPriority(Object obj) { if (obj instanceof Class clazz) { return OrderUtils.getPriority(clazz); } 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 9b5e4ecd2f94..b54b9351eb43 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 String toString() { * {@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/AnnotationTypeMapping.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java index 5bda87e8dadd..7a9b18287b2a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMapping.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,14 +28,10 @@ import java.util.List; import java.util.Map; import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Predicate; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -51,29 +47,12 @@ */ final class AnnotationTypeMapping { - private static final Log logger = LogFactory.getLog(AnnotationTypeMapping.class); - - private static final Predicate isBeanValidationConstraint = annotation -> - annotation.annotationType().getName().equals("jakarta.validation.Constraint"); - - /** - * Set used to track which convention-based annotation attribute overrides - * have already been checked. Each key is the combination of the fully - * qualified class name of a composed annotation and a meta-annotation - * that it is either present or meta-present on the composed annotation, - * separated by a dash. - * @since 6.0 - * @see #addConventionMappings() - */ - private static final Set conventionBasedOverrideCheckCache = ConcurrentHashMap.newKeySet(); - private static final MirrorSet[] EMPTY_MIRROR_SETS = new MirrorSet[0]; private static final int[] EMPTY_INT_ARRAY = new int[0]; - @Nullable - private final AnnotationTypeMapping source; + private final @Nullable AnnotationTypeMapping source; private final AnnotationTypeMapping root; @@ -83,8 +62,7 @@ final class AnnotationTypeMapping { private final List> metaTypes; - @Nullable - private final Annotation annotation; + private final @Nullable Annotation annotation; private final AttributeMethods attributes; @@ -92,8 +70,6 @@ final class AnnotationTypeMapping { private final int[] aliasMappings; - private final int[] conventionMappings; - private final int[] annotationValueMappings; private final AnnotationTypeMapping[] annotationValueSource; @@ -119,13 +95,10 @@ final class AnnotationTypeMapping { this.attributes = AttributeMethods.forAnnotationType(annotationType); this.mirrorSets = new MirrorSets(); this.aliasMappings = filledIntArray(this.attributes.size()); - this.conventionMappings = filledIntArray(this.attributes.size()); this.annotationValueMappings = filledIntArray(this.attributes.size()); this.annotationValueSource = new AnnotationTypeMapping[this.attributes.size()]; this.aliasedBy = resolveAliasedForTargets(); processAliases(); - addConventionMappings(); - addConventionAnnotationValues(); this.synthesizable = computeSynthesizableFlag(visitedAnnotationTypes); } @@ -286,95 +259,6 @@ private int getFirstRootAttributeIndex(Collection aliases) { return -1; } - private void addConventionMappings() { - if (this.distance == 0) { - return; - } - AttributeMethods rootAttributes = this.root.getAttributes(); - int[] mappings = this.conventionMappings; - Set conventionMappedAttributes = new HashSet<>(); - for (int i = 0; i < mappings.length; i++) { - String name = this.attributes.get(i).getName(); - int mapped = rootAttributes.indexOf(name); - if (!MergedAnnotation.VALUE.equals(name) && mapped != -1 && !isExplicitAttributeOverride(name)) { - conventionMappedAttributes.add(name); - mappings[i] = mapped; - MirrorSet mirrors = getMirrorSets().getAssigned(i); - if (mirrors != null) { - for (int j = 0; j < mirrors.size(); j++) { - mappings[mirrors.getAttributeIndex(j)] = mapped; - } - } - } - } - String rootAnnotationTypeName = this.root.annotationType.getName(); - String cacheKey = rootAnnotationTypeName + '-' + this.annotationType.getName(); - // We want to avoid duplicate log warnings as much as possible, without full synchronization, - // and we intentionally invoke add() before checking if any convention-based overrides were - // actually encountered in order to ensure that we add a "tracked" entry for the current cache - // key in any case. - // In addition, we do NOT want to log warnings for custom Java Bean Validation constraint - // annotations that are meta-annotated with other constraint annotations -- for example, - // @org.hibernate.validator.constraints.URL which overrides attributes in - // @jakarta.validation.constraints.Pattern. - if (conventionBasedOverrideCheckCache.add(cacheKey) && !conventionMappedAttributes.isEmpty() && - Arrays.stream(this.annotationType.getAnnotations()).noneMatch(isBeanValidationConstraint) && - logger.isWarnEnabled()) { - logger.warn(""" - Support for convention-based annotation attribute overrides is deprecated \ - and will be removed in Spring Framework 7.0. Please annotate the following \ - attributes in @%s with appropriate @AliasFor declarations: %s""" - .formatted(rootAnnotationTypeName, conventionMappedAttributes)); - } - } - - /** - * Determine if the given annotation attribute in the {@linkplain #getRoot() - * root annotation} is an explicit annotation attribute override for an - * attribute in a meta-annotation, explicit in the sense that the override - * is declared via {@link AliasFor @AliasFor}. - *

    If the named attribute does not exist in the root annotation, this - * method returns {@code false}. - * @param name the name of the annotation attribute to check - * @since 6.0 - */ - private boolean isExplicitAttributeOverride(String name) { - Method attribute = this.root.getAttributes().get(name); - if (attribute != null) { - AliasFor aliasFor = AnnotationsScanner.getDeclaredAnnotation(attribute, AliasFor.class); - return ((aliasFor != null) && - (aliasFor.annotation() != Annotation.class) && - (aliasFor.annotation() != this.root.annotationType)); - } - return false; - } - - private void addConventionAnnotationValues() { - for (int i = 0; i < this.attributes.size(); i++) { - Method attribute = this.attributes.get(i); - boolean isValueAttribute = MergedAnnotation.VALUE.equals(attribute.getName()); - AnnotationTypeMapping mapping = this; - while (mapping != null && mapping.distance > 0) { - int mapped = mapping.getAttributes().indexOf(attribute.getName()); - if (mapped != -1 && isBetterConventionAnnotationValue(i, isValueAttribute, mapping)) { - this.annotationValueMappings[i] = mapped; - this.annotationValueSource[i] = mapping; - } - mapping = mapping.source; - } - } - } - - private boolean isBetterConventionAnnotationValue(int index, boolean isValueAttribute, - AnnotationTypeMapping mapping) { - - if (this.annotationValueMappings[index] == -1) { - return true; - } - int existingDistance = this.annotationValueSource[index].distance; - return !isValueAttribute && existingDistance > mapping.distance; - } - @SuppressWarnings("unchecked") private boolean computeSynthesizableFlag(Set> visitedAnnotationTypes) { // Track that we have visited the current annotation type. @@ -392,13 +276,6 @@ private boolean computeSynthesizableFlag(Set> visite return true; } - // Uses convention-based attribute overrides in meta-annotations? - for (int index : this.conventionMappings) { - if (index != -1) { - return true; - } - } - // Has nested annotations or arrays of annotations that are synthesizable? if (getAttributes().hasNestedAnnotation()) { AttributeMethods attributeMethods = getAttributes(); @@ -481,8 +358,7 @@ AnnotationTypeMapping getRoot() { * Get the source of the mapping or {@code null}. * @return the source of the mapping */ - @Nullable - AnnotationTypeMapping getSource() { + @Nullable AnnotationTypeMapping getSource() { return this.source; } @@ -511,8 +387,7 @@ List> getMetaTypes() { * meta-annotation, or {@code null} if this is the root mapping. * @return the source annotation of the mapping */ - @Nullable - Annotation getAnnotation() { + @Nullable Annotation getAnnotation() { return this.annotation; } @@ -536,32 +411,19 @@ int getAliasMapping(int attributeIndex) { return this.aliasMappings[attributeIndex]; } - /** - * Get the related index of a convention mapped attribute, or {@code -1} - * if there is no mapping. The resulting value is the index of the attribute - * on the root annotation that can be invoked in order to obtain the actual - * value. - * @param attributeIndex the attribute index of the source attribute - * @return the mapped attribute index or {@code -1} - */ - int getConventionMapping(int attributeIndex) { - return this.conventionMappings[attributeIndex]; - } - /** * Get a mapped attribute value from the most suitable * {@link #getAnnotation() meta-annotation}. *

    The resulting value is obtained from the closest meta-annotation, - * taking into consideration both convention and alias based mapping rules. - * For root mappings, this method will always return {@code null}. + * taking into consideration alias based mapping rules. For root mappings, + * this method will always return {@code null}. * @param attributeIndex the attribute index of the source attribute * @param metaAnnotationsOnly if only meta annotations should be considered. * If this parameter is {@code false} then aliases within the annotation will * also be considered. * @return the mapped annotation value, or {@code null} */ - @Nullable - Object getMappedAnnotationValue(int attributeIndex, boolean metaAnnotationsOnly) { + @Nullable Object getMappedAnnotationValue(int attributeIndex, boolean metaAnnotationsOnly) { int mappedIndex = this.annotationValueMappings[attributeIndex]; if (mappedIndex == -1) { return null; @@ -727,8 +589,7 @@ MirrorSet get(int index) { return this.mirrorSets[index]; } - @Nullable - MirrorSet getAssigned(int attributeIndex) { + @Nullable MirrorSet getAssigned(int attributeIndex) { return this.assigned[attributeIndex]; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java index e8e07ec311d0..e488aeddd92c 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationTypeMappings.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. @@ -25,7 +25,9 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; import org.springframework.util.ConcurrentReferenceHashMap; /** @@ -34,9 +36,9 @@ * meta-annotations to ultimately provide a quick way to map the attributes of * a root {@link Annotation}. * - *

    Supports convention based merging of meta-annotations as well as implicit - * and explicit {@link AliasFor @AliasFor} aliases. Also provides information - * about mirrored attributes. + *

    Supports merging of meta-annotations as well as implicit and explicit + * {@link AliasFor @AliasFor} aliases. Also provides information about mirrored + * attributes. * *

    This class is designed to be cached so that meta-annotations only need to * be searched once, regardless of how many times they are actually used. @@ -86,7 +88,7 @@ private void addAllMappings(Class annotationType, } private void addMetaAnnotationsToQueue(Deque queue, AnnotationTypeMapping source) { - Annotation[] metaAnnotations = AnnotationsScanner.getDeclaredAnnotations(source.getAnnotationType(), false); + @Nullable Annotation[] metaAnnotations = AnnotationsScanner.getDeclaredAnnotations(source.getAnnotationType(), false); for (Annotation metaAnnotation : metaAnnotations) { if (!isMappable(source, metaAnnotation)) { continue; @@ -126,6 +128,7 @@ private void addIfPossible(Deque queue, @Nullable Annotat } } + @Contract("_, null -> false") private boolean isMappable(AnnotationTypeMapping source, @Nullable Annotation metaAnnotation) { return (metaAnnotation != null && !this.filter.matches(metaAnnotation) && !AnnotationFilter.PLAIN.matches(source.getAnnotationType()) && 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 98e912e33e0c..e17b8e12f32f 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 @@ -30,11 +30,12 @@ import java.util.NoSuchElementException; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.BridgeMethodResolver; import org.springframework.core.annotation.AnnotationTypeMapping.MirrorSets.MirrorSet; import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; @@ -189,8 +190,7 @@ public static boolean isCandidateClass(Class clazz, String annotationName) { * @since 4.0 */ @SuppressWarnings("unchecked") - @Nullable - public static A getAnnotation(Annotation annotation, Class annotationType) { + public static @Nullable A getAnnotation(Annotation annotation, Class annotationType) { // Shortcut: directly present on the element, with no merging needed? if (annotationType.isInstance(annotation)) { return synthesizeAnnotation((A) annotation, annotationType); @@ -217,8 +217,7 @@ public static A getAnnotation(Annotation annotation, Clas * @return the first matching annotation, or {@code null} if not found * @since 3.1 */ - @Nullable - public static A getAnnotation(AnnotatedElement annotatedElement, Class annotationType) { + public static @Nullable A getAnnotation(AnnotatedElement annotatedElement, Class annotationType) { // Shortcut: directly present on the element, with no merging needed? if (AnnotationFilter.PLAIN.matches(annotationType) || AnnotationsScanner.hasPlainJavaAnnotationsOnly(annotatedElement)) { @@ -249,8 +248,7 @@ private static boolean isSingleLevelPresent(MergedAnnotat * @see org.springframework.core.BridgeMethodResolver#findBridgedMethod(Method) * @see #getAnnotation(AnnotatedElement, Class) */ - @Nullable - public static A getAnnotation(Method method, Class annotationType) { + public static @Nullable A getAnnotation(Method method, Class annotationType) { Method resolvedMethod = BridgeMethodResolver.findBridgedMethod(method); return getAnnotation((AnnotatedElement) resolvedMethod, annotationType); } @@ -265,11 +263,10 @@ public static A getAnnotation(Method method, Class ann * 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 - @Nullable - public static Annotation[] getAnnotations(AnnotatedElement annotatedElement) { + @Deprecated(since = "5.2") + public static Annotation @Nullable [] getAnnotations(AnnotatedElement annotatedElement) { try { return synthesizeAnnotationArray(annotatedElement.getAnnotations(), annotatedElement); } @@ -290,11 +287,10 @@ public static Annotation[] getAnnotations(AnnotatedElement annotatedElement) { * 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 - @Nullable - public static Annotation[] getAnnotations(Method method) { + @Deprecated(since = "5.2") + public static Annotation @Nullable [] getAnnotations(Method method) { try { return synthesizeAnnotationArray(BridgeMethodResolver.findBridgedMethod(method).getAnnotations(), method); } @@ -330,9 +326,9 @@ public static Annotation[] getAnnotations(Method method) { * @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) { @@ -367,14 +363,14 @@ public static Set getRepeatableAnnotations(AnnotatedEl * @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) { RepeatableContainers repeatableContainers = (containerAnnotationType != null ? - RepeatableContainers.of(annotationType, containerAnnotationType) : + RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) : RepeatableContainers.standardRepeatables()); return MergedAnnotations.from(annotatedElement, SearchStrategy.SUPERCLASS, repeatableContainers) @@ -411,9 +407,9 @@ public static Set getRepeatableAnnotations(AnnotatedEl * @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) { @@ -448,14 +444,14 @@ public static Set getDeclaredRepeatableAnnotations(Ann * @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) { RepeatableContainers repeatableContainers = containerAnnotationType != null ? - RepeatableContainers.of(annotationType, containerAnnotationType) : + RepeatableContainers.explicitRepeatable(annotationType, containerAnnotationType) : RepeatableContainers.standardRepeatables(); return MergedAnnotations.from(annotatedElement, SearchStrategy.DIRECT, repeatableContainers) @@ -480,8 +476,7 @@ public static Set getDeclaredRepeatableAnnotations(Ann * @return the first matching annotation, or {@code null} if not found * @since 4.2 */ - @Nullable - public static A findAnnotation( + public static @Nullable A findAnnotation( AnnotatedElement annotatedElement, @Nullable Class annotationType) { if (annotationType == null) { @@ -515,8 +510,7 @@ public static A findAnnotation( * @return the first matching annotation, or {@code null} if not found * @see #getAnnotation(Method, Class) */ - @Nullable - public static A findAnnotation(Method method, @Nullable Class annotationType) { + public static @Nullable A findAnnotation(Method method, @Nullable Class annotationType) { if (annotationType == null) { return null; } @@ -555,8 +549,7 @@ public static A findAnnotation(Method method, @Nullable C * @param annotationType the type of annotation to look for * @return the first matching annotation, or {@code null} if not found */ - @Nullable - public static A findAnnotation(Class clazz, @Nullable Class annotationType) { + public static @Nullable A findAnnotation(Class clazz, @Nullable Class annotationType) { if (annotationType == null) { return null; } @@ -602,11 +595,10 @@ public static A findAnnotation(Class clazz, @Nullable * 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 - @Nullable - public static Class findAnnotationDeclaringClass( + @Deprecated(since = "5.2") + public static @Nullable Class findAnnotationDeclaringClass( Class annotationType, @Nullable Class clazz) { if (clazz == null) { @@ -639,11 +631,10 @@ public static Class findAnnotationDeclaringClass( * @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 - @Nullable - public static Class findAnnotationDeclaringClassForTypes( + @Deprecated(since = "5.2") + public static @Nullable Class findAnnotationDeclaringClassForTypes( List> annotationTypes, @Nullable Class clazz) { if (clazz == null) { @@ -693,9 +684,9 @@ public static boolean isAnnotationDeclaredLocally(Class an * 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) @@ -711,9 +702,9 @@ public static boolean isAnnotationInherited(Class annotati * @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) { @@ -782,7 +773,7 @@ public static void validateAnnotation(Annotation annotation) { * @see #getAnnotationAttributes(Annotation, boolean, boolean) * @see #getAnnotationAttributes(AnnotatedElement, Annotation, boolean, boolean) */ - public static Map getAnnotationAttributes(Annotation annotation) { + public static Map getAnnotationAttributes(Annotation annotation) { return getAnnotationAttributes(null, annotation); } @@ -800,7 +791,7 @@ public static Map getAnnotationAttributes(Annotation annotation) * corresponding attribute values as values (never {@code null}) * @see #getAnnotationAttributes(Annotation, boolean, boolean) */ - public static Map getAnnotationAttributes( + public static Map getAnnotationAttributes( Annotation annotation, boolean classValuesAsString) { return getAnnotationAttributes(annotation, classValuesAsString, false); @@ -984,8 +975,7 @@ public static void postProcessAnnotationAttributes(@Nullable Object annotatedEle } } - @Nullable - private static Object getAttributeValueForMirrorResolution(Method attribute, @Nullable Object attributes) { + private static @Nullable Object getAttributeValueForMirrorResolution(Method attribute, @Nullable Object attributes) { if (!(attributes instanceof AnnotationAttributes annotationAttributes)) { return null; } @@ -993,8 +983,7 @@ private static Object getAttributeValueForMirrorResolution(Method attribute, @Nu return (result instanceof DefaultValueHolder defaultValueHolder ? defaultValueHolder.defaultValue : result); } - @Nullable - private static Object adaptValue( + private static @Nullable Object adaptValue( @Nullable Object annotatedElement, @Nullable Object value, boolean classValuesAsString) { if (classValuesAsString) { @@ -1032,8 +1021,7 @@ private static Object adaptValue( * in which case such an exception will be rethrown * @see #getValue(Annotation, String) */ - @Nullable - public static Object getValue(Annotation annotation) { + public static @Nullable Object getValue(Annotation annotation) { return getValue(annotation, VALUE); } @@ -1046,8 +1034,7 @@ public static Object getValue(Annotation annotation) { * in which case such an exception will be rethrown * @see #getValue(Annotation) */ - @Nullable - public static Object getValue(@Nullable Annotation annotation, @Nullable String attributeName) { + public static @Nullable Object getValue(@Nullable Annotation annotation, @Nullable String attributeName) { if (annotation == null || !StringUtils.hasText(attributeName)) { return null; } @@ -1075,8 +1062,7 @@ public static Object getValue(@Nullable Annotation annotation, @Nullable String * @return the value returned from the method invocation * @since 5.3.24 */ - @Nullable - static Object invokeAnnotationMethod(Method method, @Nullable Object annotation) { + static @Nullable Object invokeAnnotationMethod(Method method, @Nullable Object annotation) { if (annotation == null) { return null; } @@ -1156,8 +1142,7 @@ private static void handleValueRetrievalFailure(Annotation annotation, Throwable * @return the default value, or {@code null} if not found * @see #getDefaultValue(Annotation, String) */ - @Nullable - public static Object getDefaultValue(Annotation annotation) { + public static @Nullable Object getDefaultValue(Annotation annotation) { return getDefaultValue(annotation, VALUE); } @@ -1168,8 +1153,7 @@ public static Object getDefaultValue(Annotation annotation) { * @return the default value of the named attribute, or {@code null} if not found * @see #getDefaultValue(Class, String) */ - @Nullable - public static Object getDefaultValue(@Nullable Annotation annotation, @Nullable String attributeName) { + public static @Nullable Object getDefaultValue(@Nullable Annotation annotation, @Nullable String attributeName) { return (annotation != null ? getDefaultValue(annotation.annotationType(), attributeName) : null); } @@ -1180,8 +1164,7 @@ public static Object getDefaultValue(@Nullable Annotation annotation, @Nullable * @return the default value, or {@code null} if not found * @see #getDefaultValue(Class, String) */ - @Nullable - public static Object getDefaultValue(Class annotationType) { + public static @Nullable Object getDefaultValue(Class annotationType) { return getDefaultValue(annotationType, VALUE); } @@ -1193,8 +1176,7 @@ public static Object getDefaultValue(Class annotationType) * @return the default value of the named attribute, or {@code null} if not found * @see #getDefaultValue(Annotation, String) */ - @Nullable - public static Object getDefaultValue( + public static @Nullable Object getDefaultValue( @Nullable Class annotationType, @Nullable String attributeName) { if (annotationType == null || !StringUtils.hasText(attributeName)) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java index f7a3d16f2df3..ef6a4a276e99 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AnnotationsProcessor.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. @@ -18,7 +18,7 @@ import java.lang.annotation.Annotation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Callback interface used to process annotations. @@ -40,8 +40,7 @@ interface AnnotationsProcessor { * @param aggregateIndex the aggregate index about to be processed * @return a {@code non-null} result if no further processing is required */ - @Nullable - default R doWithAggregate(C context, int aggregateIndex) { + default @Nullable R doWithAggregate(C context, int aggregateIndex) { return null; } @@ -55,8 +54,7 @@ default R doWithAggregate(C context, int aggregateIndex) { * {@code null} elements) * @return a {@code non-null} result if no further processing is required */ - @Nullable - R doWithAnnotations(C context, int aggregateIndex, @Nullable Object source, Annotation[] annotations); + @Nullable R doWithAnnotations(C context, int aggregateIndex, @Nullable Object source, @Nullable Annotation[] annotations); /** * Get the final result to be returned. By default this method returns @@ -64,8 +62,7 @@ default R doWithAggregate(C context, int aggregateIndex) { * @param result the last early exit result, or {@code null} if none * @return the final result to be returned to the caller */ - @Nullable - default R finish(@Nullable R result) { + default @Nullable R finish(@Nullable R result) { return result; } 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 a3d08f369bbe..a0f9a146a62a 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 @@ -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,12 +25,13 @@ import java.util.Map; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.core.BridgeMethodResolver; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.core.annotation.MergedAnnotations.Search; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -46,7 +47,7 @@ */ abstract class AnnotationsScanner { - private static final Annotation[] NO_ANNOTATIONS = {}; + private static final @Nullable Annotation[] NO_ANNOTATIONS = {}; private static final Method[] NO_METHODS = {}; @@ -54,7 +55,7 @@ abstract class AnnotationsScanner { private static final Map declaredAnnotationCache = new ConcurrentReferenceHashMap<>(256); - private static final Map, Method[]> baseTypeMethodsCache = + private static final Map, @Nullable Method[]> baseTypeMethodsCache = new ConcurrentReferenceHashMap<>(256); @@ -75,16 +76,14 @@ private AnnotationsScanner() { * @param processor the processor that receives the annotations * @return the result of {@link AnnotationsProcessor#finish(Object)} */ - @Nullable - static R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy, + static @Nullable R scan(C context, AnnotatedElement source, SearchStrategy searchStrategy, Predicate> searchEnclosingClass, AnnotationsProcessor processor) { R result = process(context, source, searchStrategy, searchEnclosingClass, processor); return processor.finish(result); } - @Nullable - private static R process(C context, AnnotatedElement source, + private static @Nullable R process(C context, AnnotatedElement source, SearchStrategy searchStrategy, Predicate> searchEnclosingClass, AnnotationsProcessor processor) { @@ -97,8 +96,7 @@ private static R process(C context, AnnotatedElement source, return processElement(context, source, processor); } - @Nullable - private static R processClass(C context, Class source, SearchStrategy searchStrategy, + private static @Nullable R processClass(C context, Class source, SearchStrategy searchStrategy, Predicate> searchEnclosingClass, AnnotationsProcessor processor) { return switch (searchStrategy) { @@ -109,15 +107,14 @@ private static R processClass(C context, Class source, SearchStrategy }; } - @Nullable - private static R processClassInheritedAnnotations(C context, Class source, + private static @Nullable R processClassInheritedAnnotations(C context, Class source, AnnotationsProcessor processor) { try { if (isWithoutHierarchy(source, Search.never)) { return processElement(context, source, processor); } - Annotation[] relevant = null; + @Nullable Annotation[] relevant = null; int remaining = Integer.MAX_VALUE; int aggregateIndex = 0; Class root = source; @@ -126,7 +123,7 @@ private static R processClassInheritedAnnotations(C context, Class sou if (result != null) { return result; } - Annotation[] declaredAnns = getDeclaredAnnotations(source, true); + @Nullable Annotation[] declaredAnns = getDeclaredAnnotations(source, true); if (declaredAnns.length > 0) { if (relevant == null) { relevant = root.getAnnotations(); @@ -136,6 +133,7 @@ private static R processClassInheritedAnnotations(C context, Class sou if (declaredAnns[i] != null) { boolean isRelevant = false; for (int relevantIndex = 0; relevantIndex < relevant.length; relevantIndex++) { + //noinspection DataFlowIssue if (relevant[relevantIndex] != null && declaredAnns[i].annotationType() == relevant[relevantIndex].annotationType()) { isRelevant = true; @@ -164,8 +162,7 @@ private static R processClassInheritedAnnotations(C context, Class sou return null; } - @Nullable - private static R processClassHierarchy(C context, Class source, + private static @Nullable R processClassHierarchy(C context, Class source, AnnotationsProcessor processor, boolean includeInterfaces, Predicate> searchEnclosingClass) { @@ -173,8 +170,7 @@ private static R processClassHierarchy(C context, Class source, includeInterfaces, searchEnclosingClass); } - @Nullable - private static R processClassHierarchy(C context, int[] aggregateIndex, Class source, + private static @Nullable R processClassHierarchy(C context, int[] aggregateIndex, Class source, AnnotationsProcessor processor, boolean includeInterfaces, Predicate> searchEnclosingClass) { @@ -186,7 +182,7 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C if (hasPlainJavaAnnotationsOnly(source)) { return null; } - Annotation[] annotations = getDeclaredAnnotations(source, false); + @Nullable Annotation[] annotations = getDeclaredAnnotations(source, false); result = processor.doWithAnnotations(context, aggregateIndex[0], source, annotations); if (result != null) { return result; @@ -236,8 +232,7 @@ private static R processClassHierarchy(C context, int[] aggregateIndex, C return null; } - @Nullable - private static R processMethod(C context, Method source, + private static @Nullable R processMethod(C context, Method source, SearchStrategy searchStrategy, AnnotationsProcessor processor) { return switch (searchStrategy) { @@ -249,8 +244,7 @@ private static R processMethod(C context, Method source, }; } - @Nullable - private static R processMethodInheritedAnnotations(C context, Method source, + private static @Nullable R processMethodInheritedAnnotations(C context, Method source, AnnotationsProcessor processor) { try { @@ -264,8 +258,7 @@ private static R processMethodInheritedAnnotations(C context, Method sour return null; } - @Nullable - private static R processMethodHierarchy(C context, int[] aggregateIndex, + private static @Nullable R processMethodHierarchy(C context, int[] aggregateIndex, Class sourceClass, AnnotationsProcessor processor, Method rootMethod, boolean includeInterfaces) { @@ -328,16 +321,18 @@ private static R processMethodHierarchy(C context, int[] aggregateIndex, return null; } - private static Method[] getBaseTypeMethods(C context, Class baseType) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private static @Nullable Method[] getBaseTypeMethods(C context, Class baseType) { if (baseType == Object.class || hasPlainJavaAnnotationsOnly(baseType)) { return NO_METHODS; } - Method[] methods = baseTypeMethodsCache.get(baseType); + @Nullable Method[] methods = baseTypeMethodsCache.get(baseType); if (methods == null) { methods = ReflectionUtils.getDeclaredMethods(baseType); int cleared = 0; for (int i = 0; i < methods.length; i++) { + //noinspection DataFlowIssue if (Modifier.isPrivate(methods[i].getModifiers()) || hasPlainJavaAnnotationsOnly(methods[i]) || getDeclaredAnnotations(methods[i], false).length == 0) { @@ -355,21 +350,18 @@ private static Method[] getBaseTypeMethods(C context, Class baseType) { 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( @@ -390,18 +382,17 @@ private static boolean hasSameGenericTypeParameters( return true; } - @Nullable - private static R processMethodAnnotations(C context, int aggregateIndex, Method source, + private static @Nullable R processMethodAnnotations(C context, int aggregateIndex, Method source, AnnotationsProcessor processor) { - Annotation[] annotations = getDeclaredAnnotations(source, false); + @Nullable Annotation[] annotations = getDeclaredAnnotations(source, false); R result = processor.doWithAnnotations(context, aggregateIndex, source, annotations); if (result != null) { return result; } Method bridgedMethod = BridgeMethodResolver.findBridgedMethod(source); if (bridgedMethod != source) { - Annotation[] bridgedAnnotations = getDeclaredAnnotations(bridgedMethod, true); + @Nullable Annotation[] bridgedAnnotations = getDeclaredAnnotations(bridgedMethod, true); for (int i = 0; i < bridgedAnnotations.length; i++) { if (ObjectUtils.containsElement(annotations, bridgedAnnotations[i])) { bridgedAnnotations[i] = null; @@ -412,8 +403,7 @@ private static R processMethodAnnotations(C context, int aggregateIndex, return null; } - @Nullable - private static R processElement(C context, AnnotatedElement source, + private static @Nullable R processElement(C context, AnnotatedElement source, AnnotationsProcessor processor) { try { @@ -428,9 +418,8 @@ private static R processElement(C context, AnnotatedElement source, } @SuppressWarnings("unchecked") - @Nullable - static A getDeclaredAnnotation(AnnotatedElement source, Class annotationType) { - Annotation[] annotations = getDeclaredAnnotations(source, false); + static @Nullable A getDeclaredAnnotation(AnnotatedElement source, Class annotationType) { + @Nullable Annotation[] annotations = getDeclaredAnnotations(source, false); for (Annotation annotation : annotations) { if (annotation != null && annotationType == annotation.annotationType()) { return (A) annotation; @@ -439,9 +428,10 @@ static A getDeclaredAnnotation(AnnotatedElement source, C return null; } - static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defensive) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + static @Nullable Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defensive) { boolean cached = false; - Annotation[] annotations = declaredAnnotationCache.get(source); + @Nullable Annotation[] annotations = declaredAnnotationCache.get(source); if (annotations != null) { cached = true; } @@ -451,6 +441,7 @@ static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defe boolean allIgnored = true; for (int i = 0; i < annotations.length; i++) { Annotation annotation = annotations[i]; + //noinspection DataFlowIssue if (isIgnorable(annotation.annotationType()) || !AttributeMethods.forAnnotationType(annotation.annotationType()).canLoad(annotation)) { annotations[i] = null; @@ -461,6 +452,7 @@ static Annotation[] getDeclaredAnnotations(AnnotatedElement source, boolean defe } annotations = (allIgnored ? NO_ANNOTATIONS : annotations); if (source instanceof Class || source instanceof Member) { + //noinspection NullableProblems declaredAnnotationCache.put(source, annotations); cached = true; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java index fb8c2bda38c7..d805bfaa3d6f 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/AttributeMethods.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,8 @@ import java.util.Comparator; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; @@ -49,8 +50,7 @@ final class AttributeMethods { }; - @Nullable - private final Class annotationType; + private final @Nullable Class annotationType; private final Method[] attributeMethods; @@ -155,8 +155,7 @@ private void assertAnnotation(Annotation annotation) { * @param name the attribute name to find * @return the attribute method or {@code null} */ - @Nullable - Method get(String name) { + @Nullable Method get(String name) { int index = indexOf(name); return (index != -1 ? this.attributeMethods[index] : null); } @@ -251,11 +250,13 @@ static AttributeMethods forAnnotationType(@Nullable Class return cache.computeIfAbsent(annotationType, AttributeMethods::compute); } + @SuppressWarnings("NullAway") // Dataflow analysis limitation private static AttributeMethods compute(Class annotationType) { Method[] methods = annotationType.getDeclaredMethods(); int size = methods.length; for (int i = 0; i < methods.length; i++) { if (!isAttributeMethod(methods[i])) { + //noinspection DataFlowIssue methods[i] = null; size--; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java index 23178ce385a7..5bcdfbae37bb 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/IntrospectionFailureLogger.java @@ -18,8 +18,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Log facade used to handle annotation introspection failures (in particular @@ -55,8 +54,7 @@ public void log(String message) { }; - @Nullable - private static Log logger; + private static @Nullable Log logger; void log(String message, @Nullable Object source, Exception ex) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java index 6d9c1bcf5de3..bdabde5be41c 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotation.java @@ -28,8 +28,9 @@ import java.util.function.Function; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; /** * A single merged annotation returned from a {@link MergedAnnotations} @@ -133,8 +134,7 @@ public interface MergedAnnotation { * {@link #getRoot() root}. * @return the source, or {@code null} */ - @Nullable - Object getSource(); + @Nullable Object getSource(); /** * Get the source of the meta-annotation, or {@code null} if the @@ -144,8 +144,7 @@ public interface MergedAnnotation { * @return the meta-annotation source or {@code null} * @see #getRoot() */ - @Nullable - MergedAnnotation getMetaSource(); + @Nullable MergedAnnotation getMetaSource(); /** * Get the root annotation, i.e. the {@link #getDistance() distance} {@code 0} @@ -491,8 +490,6 @@ MergedAnnotation[] getAnnotationArray(String attribute * it has not already been synthesized and one of the following is true. *

    diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java index d09369f9165e..5a303534aa37 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationCollectors.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. @@ -26,6 +26,8 @@ import java.util.stream.Collector; import java.util.stream.Collector.Characteristics; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.MergedAnnotation.Adapt; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -108,10 +110,10 @@ private MergedAnnotationCollectors() { * annotations into a {@link LinkedMultiValueMap} * @see #toMultiValueMap(Function, MergedAnnotation.Adapt...) */ - public static Collector, ?, MultiValueMap> toMultiValueMap( + public static Collector, ? extends @Nullable Object, @Nullable MultiValueMap> toMultiValueMap( Adapt... adaptations) { - return toMultiValueMap(Function.identity(), adaptations); + return toMultiValueMap((MultiValueMap t) -> t, adaptations); } /** @@ -126,14 +128,14 @@ private MergedAnnotationCollectors() { * annotations into a {@link LinkedMultiValueMap} * @see #toMultiValueMap(MergedAnnotation.Adapt...) */ - public static Collector, ?, MultiValueMap> toMultiValueMap( - Function, MultiValueMap> finisher, + public static Collector, ? extends @Nullable Object, @Nullable MultiValueMap> toMultiValueMap( + Function, @Nullable MultiValueMap> finisher, Adapt... adaptations) { Characteristics[] characteristics = (isSameInstance(finisher, Function.identity()) ? IDENTITY_FINISH_CHARACTERISTICS : NO_CHARACTERISTICS); return Collector.of(LinkedMultiValueMap::new, - (map, annotation) -> annotation.asMap(adaptations).forEach(map::add), + (MultiValueMap map, MergedAnnotation annotation) -> annotation.asMap(adaptations).forEach(map::add), MergedAnnotationCollectors::combiner, finisher, characteristics); } @@ -157,7 +159,7 @@ private static > C combiner(C collection, C additions *

    This method is only invoked if the {@link java.util.stream.Stream} is * processed in {@linkplain java.util.stream.Stream#parallel() parallel}. */ - private static MultiValueMap combiner(MultiValueMap map, MultiValueMap additions) { + private static MultiValueMap combiner(MultiValueMap map, MultiValueMap additions) { map.addAll(additions); return map; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java index 95c2bd6a5209..9568829b9a1a 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationPredicates.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,7 +23,6 @@ import java.util.function.Function; import java.util.function.Predicate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -125,7 +124,7 @@ private static class FirstRunOfPredicate implements Predic private boolean hasLastValue; - @Nullable + @SuppressWarnings("NullAway.Init") private Object lastValue; FirstRunOfPredicate(Function, ?> valueExtractor) { @@ -134,7 +133,7 @@ private static class FirstRunOfPredicate implements Predic } @Override - public boolean test(@Nullable MergedAnnotation annotation) { + public boolean test(MergedAnnotation annotation) { if (!this.hasLastValue) { this.hasLastValue = true; this.lastValue = this.valueExtractor.apply(annotation); @@ -162,7 +161,7 @@ private static class UniquePredicate implements Predica } @Override - public boolean test(@Nullable MergedAnnotation annotation) { + public boolean test(MergedAnnotation annotation) { K key = this.keyExtractor.apply(annotation); return this.seen.add(key); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java index 96bcd5ae5074..2267b0f829b4 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotations.java @@ -24,7 +24,8 @@ import java.util.function.Predicate; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -35,10 +36,9 @@ * "merged" from different source values, typically: * *

    * diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java index 01c0bafd1b21..6908fa19560d 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MergedAnnotationsCollection.java @@ -26,7 +26,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -155,8 +156,7 @@ public MergedAnnotation get(String annotationType, } @SuppressWarnings("unchecked") - @Nullable - private MergedAnnotation find(Object requiredType, + private @Nullable MergedAnnotation find(Object requiredType, @Nullable Predicate> predicate, @Nullable MergedAnnotationSelector selector) { @@ -222,8 +222,7 @@ static MergedAnnotations of(Collection> annotations) { private class AnnotationsSpliterator implements Spliterator> { - @Nullable - private final Object requiredType; + private final @Nullable Object requiredType; private final int[] mappingCursors; @@ -259,8 +258,7 @@ public boolean tryAdvance(Consumer> action) { return false; } - @Nullable - private AnnotationTypeMapping getNextSuitableMapping(int annotationIndex) { + private @Nullable AnnotationTypeMapping getNextSuitableMapping(int annotationIndex) { AnnotationTypeMapping mapping; do { mapping = getMapping(annotationIndex, this.mappingCursors[annotationIndex]); @@ -273,15 +271,13 @@ private AnnotationTypeMapping getNextSuitableMapping(int annotationIndex) { return null; } - @Nullable - private AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { + private @Nullable AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { AnnotationTypeMappings mappings = MergedAnnotationsCollection.this.mappings[annotationIndex]; return (mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null); } - @Nullable @SuppressWarnings("unchecked") - private MergedAnnotation createMergedAnnotationIfPossible(int annotationIndex, int mappingIndex) { + private @Nullable MergedAnnotation createMergedAnnotationIfPossible(int annotationIndex, int mappingIndex) { MergedAnnotation root = annotations[annotationIndex]; if (mappingIndex == 0) { return (MergedAnnotation) root; @@ -293,8 +289,7 @@ private MergedAnnotation createMergedAnnotationIfPossible(int annotationIndex } @Override - @Nullable - public Spliterator> trySplit() { + public @Nullable Spliterator> trySplit() { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java index 0c7c9abad61a..1b8840bf8831 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/MissingMergedAnnotation.java @@ -25,7 +25,7 @@ import java.util.function.Function; import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An {@link AbstractMergedAnnotation} used as the implementation of @@ -56,14 +56,12 @@ public boolean isPresent() { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return null; } @Override - @Nullable - public MergedAnnotation getMetaSource() { + public @Nullable MergedAnnotation getMetaSource() { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java index 41db68d95cf2..f3f3031f811b 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/OrderUtils.java @@ -19,8 +19,9 @@ import java.lang.reflect.AnnotatedElement; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; /** @@ -66,8 +67,7 @@ public static int getOrder(Class type, int defaultOrder) { * @return the priority value, or the specified default order if none can be found * @see #getPriority(Class) */ - @Nullable - public static Integer getOrder(Class type, @Nullable Integer defaultOrder) { + public static @Nullable Integer getOrder(Class type, @Nullable Integer defaultOrder) { Integer order = getOrder(type); return (order != null ? order : defaultOrder); } @@ -79,8 +79,7 @@ public static Integer getOrder(Class type, @Nullable Integer defaultOrder) { * @return the order value, or {@code null} if none can be found * @see #getPriority(Class) */ - @Nullable - public static Integer getOrder(Class type) { + public static @Nullable Integer getOrder(Class type) { return getOrder((AnnotatedElement) type); } @@ -91,8 +90,7 @@ public static Integer getOrder(Class type) { * @return the order value, or {@code null} if none can be found * @since 5.3 */ - @Nullable - public static Integer getOrder(AnnotatedElement element) { + public static @Nullable Integer getOrder(AnnotatedElement element) { return getOrderFromAnnotations(element, MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY)); } @@ -104,8 +102,7 @@ public static Integer getOrder(AnnotatedElement element) { * @param annotations the annotation to consider * @return the order value, or {@code null} if none can be found */ - @Nullable - static Integer getOrderFromAnnotations(AnnotatedElement element, MergedAnnotations annotations) { + static @Nullable Integer getOrderFromAnnotations(AnnotatedElement element, MergedAnnotations annotations) { if (!(element instanceof Class)) { return findOrder(annotations); } @@ -118,8 +115,7 @@ static Integer getOrderFromAnnotations(AnnotatedElement element, MergedAnnotatio return result; } - @Nullable - private static Integer findOrder(MergedAnnotations annotations) { + private static @Nullable Integer findOrder(MergedAnnotations annotations) { MergedAnnotation orderAnnotation = annotations.get(Order.class); if (orderAnnotation.isPresent()) { return orderAnnotation.getInt(MergedAnnotation.VALUE); @@ -137,8 +133,7 @@ private static Integer findOrder(MergedAnnotations annotations) { * @param type the type to handle * @return the priority value if the annotation is declared, or {@code null} if none */ - @Nullable - public static Integer getPriority(Class type) { + public static @Nullable Integer getPriority(Class type) { return MergedAnnotations.from(type, SearchStrategy.TYPE_HIERARCHY).get(JAKARTA_PRIORITY_ANNOTATION) .getValue(MergedAnnotation.VALUE, Integer.class).orElse(null); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java b/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java index c050a695f4fc..2e82f189dc13 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/PackagesAnnotationFilter.java @@ -18,7 +18,8 @@ import java.util.Arrays; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java index 9f7bf61ad2d6..82717917b2db 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/RepeatableContainers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,22 +22,46 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; /** - * Strategy used to determine annotations that act as containers for other - * annotations. The {@link #standardRepeatables()} method provides a default - * strategy that respects Java's {@link Repeatable @Repeatable} support and - * should be suitable for most situations. + * Strategy used to find repeatable annotations within container annotations. + * + *

    {@link #standardRepeatables() RepeatableContainers.standardRepeatables()} + * provides a default strategy that respects Java's {@link Repeatable @Repeatable} + * support and is suitable for most situations. + * + *

    If you need to register repeatable annotation types that do not make use of + * {@code @Repeatable}, you should typically use {@code standardRepeatables()} + * combined with {@link #plus(Class, Class)}. Note that multiple invocations of + * {@code plus()} can be chained together to register multiple repeatable/container + * type pairs. For example: + * + *

    + * RepeatableContainers repeatableContainers =
    + *     RepeatableContainers.standardRepeatables()
    + *         .plus(MyRepeatable1.class, MyContainer1.class)
    + *         .plus(MyRepeatable2.class, MyContainer2.class);
    * - *

    The {@link #of} method can be used to register relationships for - * annotations that do not wish to use {@link Repeatable @Repeatable}. + *

    For special use cases where you are certain that you do not need Java's + * {@code @Repeatable} support, you can use {@link #explicitRepeatable(Class, Class) + * RepeatableContainers.explicitRepeatable()} to create an instance of + * {@code RepeatableContainers} that only supports explicit repeatable/container + * type pairs. As with {@code standardRepeatables()}, {@code plus()} can be used + * to register additional repeatable/container type pairs. For example: * - *

    To completely disable repeatable support use {@link #none()}. + *

    + * RepeatableContainers repeatableContainers =
    + *     RepeatableContainers.explicitRepeatable(MyRepeatable1.class, MyContainer1.class)
    + *         .plus(MyRepeatable2.class, MyContainer2.class);
    + * + *

    To completely disable repeatable annotation support use + * {@link #none() RepeatableContainers.none()}. * * @author Phillip Webb * @author Sam Brannen @@ -47,32 +71,54 @@ public abstract class RepeatableContainers { static final Map, Object> cache = new ConcurrentReferenceHashMap<>(); - @Nullable - private final RepeatableContainers parent; + private final @Nullable RepeatableContainers parent; private RepeatableContainers(@Nullable RepeatableContainers parent) { this.parent = parent; } + /** + * Register a pair of repeatable and container annotation types. + *

    See the {@linkplain RepeatableContainers class-level javadoc} for examples. + * @param repeatable the repeatable annotation type + * @param container the container annotation type + * @return a new {@code RepeatableContainers} instance that is chained to + * the current instance + * @since 7.0 + */ + public final RepeatableContainers plus(Class repeatable, + Class container) { + + return new ExplicitRepeatableContainer(this, repeatable, container); + } /** - * Add an additional explicit relationship between a container and - * repeatable annotation. - *

    WARNING: the arguments supplied to this method are in the reverse order - * of those supplied to {@link #of(Class, Class)}. + * Register a pair of container and repeatable annotation types. + *

    WARNING: The arguments supplied to this method are in + * the reverse order of those supplied to {@link #plus(Class, Class)}, + * {@link #explicitRepeatable(Class, Class)}, and {@link #of(Class, Class)}. * @param container the container annotation type * @param repeatable the repeatable annotation type - * @return a new {@link RepeatableContainers} instance + * @return a new {@code RepeatableContainers} instance that is chained to + * the current instance + * @deprecated as of Spring Framework 7.0, in favor of {@link #plus(Class, Class)} */ + @Deprecated(since = "7.0") public RepeatableContainers and(Class container, Class repeatable) { - return new ExplicitRepeatableContainer(this, repeatable, container); + return plus(repeatable, container); } - @Nullable - Annotation[] findRepeatedAnnotations(Annotation annotation) { + /** + * Find repeated annotations contained in the supplied {@code annotation}. + * @param annotation the candidate container annotation + * @return the repeated annotations found in the supplied container annotation + * (potentially an empty array), or {@code null} if the supplied annotation is + * not a supported container annotation + */ + Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) { if (this.parent == null) { return null; } @@ -99,41 +145,92 @@ public int hashCode() { /** - * Create a {@link RepeatableContainers} instance that searches using Java's - * {@link Repeatable @Repeatable} annotation. - * @return a {@link RepeatableContainers} instance + * Create a {@link RepeatableContainers} instance that searches for repeated + * annotations according to the semantics of Java's {@link Repeatable @Repeatable} + * annotation. + *

    See the {@linkplain RepeatableContainers class-level javadoc} for examples. + * @return a {@code RepeatableContainers} instance that supports {@code @Repeatable} + * @see #plus(Class, Class) */ public static RepeatableContainers standardRepeatables() { return StandardRepeatableContainers.INSTANCE; } /** - * Create a {@link RepeatableContainers} instance that uses predefined - * repeatable and container types. - *

    WARNING: the arguments supplied to this method are in the reverse order - * of those supplied to {@link #and(Class, Class)}. + * Create a {@link RepeatableContainers} instance that searches for repeated + * annotations by taking into account the supplied repeatable and container + * annotation types. + *

    WARNING: The {@code RepeatableContainers} instance + * returned by this factory method does not respect Java's + * {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()} + * for standard {@code @Repeatable} support, optionally combined with + * {@link #plus(Class, Class)}. + *

    If the supplied container annotation type is not {@code null}, it must + * declare a {@code value} attribute returning an array of repeatable + * annotations. If the supplied container annotation type is {@code null}, the + * container will be deduced by inspecting the {@code @Repeatable} annotation + * on the {@code repeatable} annotation type. + *

    See the {@linkplain RepeatableContainers class-level javadoc} for examples. * @param repeatable the repeatable annotation type - * @param container the container annotation type or {@code null}. If specified, - * this annotation must declare a {@code value} attribute returning an array - * of repeatable annotations. If not specified, the container will be - * deduced by inspecting the {@code @Repeatable} annotation on - * {@code repeatable}. - * @return a {@link RepeatableContainers} instance + * @param container the container annotation type or {@code null} + * @return a {@code RepeatableContainers} instance that does not support + * {@link Repeatable @Repeatable} * @throws IllegalArgumentException if the supplied container type is * {@code null} and the annotation type is not a repeatable annotation * @throws AnnotationConfigurationException if the supplied container type * is not a properly configured container for a repeatable annotation + * @since 7.0 + * @see #standardRepeatables() + * @see #plus(Class, Class) */ - public static RepeatableContainers of( + public static RepeatableContainers explicitRepeatable( Class repeatable, @Nullable Class container) { return new ExplicitRepeatableContainer(null, repeatable, container); } + /** + * Create a {@link RepeatableContainers} instance that searches for repeated + * annotations by taking into account the supplied repeatable and container + * annotation types. + *

    WARNING: The {@code RepeatableContainers} instance + * returned by this factory method does not respect Java's + * {@link Repeatable @Repeatable} support. Use {@link #standardRepeatables()} + * for standard {@code @Repeatable} support, optionally combined with + * {@link #plus(Class, Class)}. + *

    WARNING: The arguments supplied to this method are in + * the reverse order of those supplied to {@link #and(Class, Class)}. + *

    If the supplied container annotation type is not {@code null}, it must + * declare a {@code value} attribute returning an array of repeatable + * annotations. If the supplied container annotation type is {@code null}, the + * container will be deduced by inspecting the {@code @Repeatable} annotation + * on the {@code repeatable} annotation type. + * @param repeatable the repeatable annotation type + * @param container the container annotation type or {@code null} + * @return a {@code RepeatableContainers} instance that does not support + * {@link Repeatable @Repeatable} + * @throws IllegalArgumentException if the supplied container type is + * {@code null} and the annotation type is not a repeatable annotation + * @throws AnnotationConfigurationException if the supplied container type + * is not a properly configured container for a repeatable annotation + * @deprecated as of Spring Framework 7.0, in favor of {@link #explicitRepeatable(Class, Class)} + */ + @Deprecated(since = "7.0") + public static RepeatableContainers of( + Class repeatable, @Nullable Class container) { + + return explicitRepeatable(repeatable, container); + } + /** * Create a {@link RepeatableContainers} instance that does not support any * repeatable annotations. - * @return a {@link RepeatableContainers} instance + *

    Note, however, that {@link #plus(Class, Class)} may still be invoked on + * the {@code RepeatableContainers} instance returned from this method. + *

    See the {@linkplain RepeatableContainers class-level javadoc} for examples + * and further details. + * @return a {@code RepeatableContainers} instance that does not support + * repeatable annotations */ public static RepeatableContainers none() { return NoRepeatableContainers.INSTANCE; @@ -155,8 +252,7 @@ private static class StandardRepeatableContainers extends RepeatableContainers { } @Override - @Nullable - Annotation[] findRepeatedAnnotations(Annotation annotation) { + Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) { Method method = getRepeatedAnnotationsMethod(annotation.annotationType()); if (method != null) { return (Annotation[]) AnnotationUtils.invokeAnnotationMethod(method, annotation); @@ -164,8 +260,7 @@ Annotation[] findRepeatedAnnotations(Annotation annotation) { return super.findRepeatedAnnotations(annotation); } - @Nullable - private static Method getRepeatedAnnotationsMethod(Class annotationType) { + private static @Nullable Method getRepeatedAnnotationsMethod(Class annotationType) { Object result = cache.computeIfAbsent(annotationType, StandardRepeatableContainers::computeRepeatedAnnotationsMethod); return (result != NONE ? (Method) result : null); @@ -214,7 +309,7 @@ private static class ExplicitRepeatableContainer extends RepeatableContainers { throw new NoSuchMethodException("No value method found"); } Class returnType = valueMethod.getReturnType(); - if (!returnType.isArray() || returnType.componentType() != repeatable) { + if (returnType.componentType() != repeatable) { throw new AnnotationConfigurationException( "Container type [%s] must declare a 'value' attribute for an array of type [%s]" .formatted(container.getName(), repeatable.getName())); @@ -241,8 +336,7 @@ private Class deduceContainer(Class } @Override - @Nullable - Annotation[] findRepeatedAnnotations(Annotation annotation) { + Annotation @Nullable [] findRepeatedAnnotations(Annotation annotation) { if (this.container.isAssignableFrom(annotation.annotationType())) { return (Annotation[]) AnnotationUtils.invokeAnnotationMethod(this.valueMethod, annotation); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java index 2319be3b6d03..560b74a844c9 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/SynthesizedMergedAnnotationInvocationHandler.java @@ -26,7 +26,8 @@ import java.util.NoSuchElementException; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -54,11 +55,9 @@ final class SynthesizedMergedAnnotationInvocationHandler i private final Map valueCache = new ConcurrentHashMap<>(8); - @Nullable - private volatile Integer hashCode; + private volatile @Nullable Integer hashCode; - @Nullable - private volatile String string; + private volatile @Nullable String string; private SynthesizedMergedAnnotationInvocationHandler(MergedAnnotation annotation, Class type) { diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java index 4e3d98bdc7f4..a8a7c887f546 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotation.java @@ -29,7 +29,8 @@ import java.util.function.Function; import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -82,14 +83,11 @@ final class TypeMappedAnnotation extends AbstractMergedAnn private final AnnotationTypeMapping mapping; - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; - @Nullable - private final Object source; + private final @Nullable Object source; - @Nullable - private final Object rootAttributes; + private final @Nullable Object rootAttributes; private final ValueExtractor valueExtractor; @@ -97,8 +95,7 @@ final class TypeMappedAnnotation extends AbstractMergedAnn private final boolean useMergedValues; - @Nullable - private final Predicate attributeFilter; + private final @Nullable Predicate attributeFilter; private final int[] resolvedRootMirrors; @@ -114,7 +111,7 @@ private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoade private TypeMappedAnnotation(AnnotationTypeMapping mapping, @Nullable ClassLoader classLoader, @Nullable Object source, @Nullable Object rootAttributes, ValueExtractor valueExtractor, - int aggregateIndex, @Nullable int[] resolvedRootMirrors) { + int aggregateIndex, int @Nullable [] resolvedRootMirrors) { this.mapping = mapping; this.classLoader = classLoader; @@ -175,14 +172,12 @@ public int getAggregateIndex() { } @Override - @Nullable - public Object getSource() { + public @Nullable Object getSource() { return this.source; } @Override - @Nullable - public MergedAnnotation getMetaSource() { + public @Nullable MergedAnnotation getMetaSource() { AnnotationTypeMapping metaSourceMapping = this.mapping.getSource(); if (metaSourceMapping == null) { return null; @@ -204,7 +199,7 @@ public MergedAnnotation getRoot() { @Override public boolean hasDefaultValue(String attributeName) { int attributeIndex = getAttributeIndex(attributeName, true); - Object value = getValue(attributeIndex, true, false); + Object value = getValue(attributeIndex, false); return (value == null || this.mapping.isEquivalentToDefaultValue(attributeIndex, value, this.valueExtractor)); } @@ -366,8 +361,7 @@ private boolean isSynthesizable(Annotation annotation) { } @Override - @Nullable - protected T getAttributeValue(String attributeName, Class type) { + protected @Nullable T getAttributeValue(String attributeName, Class type) { int attributeIndex = getAttributeIndex(attributeName, false); return (attributeIndex != -1 ? getValue(attributeIndex, type) : null); } @@ -375,30 +369,25 @@ protected T getAttributeValue(String attributeName, Class type) { private Object getRequiredValue(int attributeIndex, String attributeName) { Object value = getValue(attributeIndex, Object.class); if (value == null) { - throw new NoSuchElementException("No element at attribute index " - + attributeIndex + " for name " + attributeName); + throw new NoSuchElementException("No element at attribute index " + + attributeIndex + " for name " + attributeName); } return value; } - @Nullable - private T getValue(int attributeIndex, Class type) { + private @Nullable T getValue(int attributeIndex, Class type) { Method attribute = this.mapping.getAttributes().get(attributeIndex); - Object value = getValue(attributeIndex, true, false); + Object value = getValue(attributeIndex, false); if (value == null) { value = attribute.getDefaultValue(); } return adapt(attribute, value, type); } - @Nullable - private Object getValue(int attributeIndex, boolean useConventionMapping, boolean forMirrorResolution) { + private @Nullable Object getValue(int attributeIndex, boolean forMirrorResolution) { AnnotationTypeMapping mapping = this.mapping; if (this.useMergedValues) { int mappedIndex = this.mapping.getAliasMapping(attributeIndex); - if (mappedIndex == -1 && useConventionMapping) { - mappedIndex = this.mapping.getConventionMapping(attributeIndex); - } if (mappedIndex != -1) { mapping = mapping.getRoot(); attributeIndex = mappedIndex; @@ -419,8 +408,7 @@ private Object getValue(int attributeIndex, boolean useConventionMapping, boolea return getValueFromMetaAnnotation(attributeIndex, forMirrorResolution); } - @Nullable - private Object getValueFromMetaAnnotation(int attributeIndex, boolean forMirrorResolution) { + private @Nullable Object getValueFromMetaAnnotation(int attributeIndex, boolean forMirrorResolution) { Object value = null; if (this.useMergedValues || forMirrorResolution) { value = this.mapping.getMappedAnnotationValue(attributeIndex, forMirrorResolution); @@ -432,16 +420,13 @@ private Object getValueFromMetaAnnotation(int attributeIndex, boolean forMirrorR return value; } - @Nullable - private Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) { + private @Nullable Object getValueForMirrorResolution(Method attribute, @Nullable Object annotation) { int attributeIndex = this.mapping.getAttributes().indexOf(attribute); - boolean valueAttribute = VALUE.equals(attribute.getName()); - return getValue(attributeIndex, !valueAttribute, true); + return getValue(attributeIndex, true); } @SuppressWarnings("unchecked") - @Nullable - private T adapt(Method attribute, @Nullable Object value, Class type) { + private @Nullable T adapt(Method attribute, @Nullable Object value, Class type) { if (value == null) { return null; } @@ -585,8 +570,7 @@ private boolean isFiltered(String attributeName) { return false; } - @Nullable - private ClassLoader getClassLoader() { + private @Nullable ClassLoader getClassLoader() { if (this.classLoader != null) { return this.classLoader; } @@ -619,8 +603,7 @@ static MergedAnnotation of( mappings.get(0), classLoader, source, attributes, TypeMappedAnnotation::extractFromMap, 0); } - @Nullable - static TypeMappedAnnotation createIfPossible( + static @Nullable TypeMappedAnnotation createIfPossible( AnnotationTypeMapping mapping, MergedAnnotation annotation, IntrospectionFailureLogger logger) { if (annotation instanceof TypeMappedAnnotation typeMappedAnnotation) { @@ -633,8 +616,7 @@ static TypeMappedAnnotation createIfPossible( annotation.getAggregateIndex(), logger); } - @Nullable - static TypeMappedAnnotation createIfPossible( + static @Nullable TypeMappedAnnotation createIfPossible( AnnotationTypeMapping mapping, @Nullable Object source, Annotation annotation, int aggregateIndex, IntrospectionFailureLogger logger) { @@ -642,8 +624,7 @@ static TypeMappedAnnotation createIfPossible( AnnotationUtils::invokeAnnotationMethod, aggregateIndex, logger); } - @Nullable - private static TypeMappedAnnotation createIfPossible( + private static @Nullable TypeMappedAnnotation createIfPossible( AnnotationTypeMapping mapping, @Nullable Object source, @Nullable Object rootAttribute, ValueExtractor valueExtractor, int aggregateIndex, IntrospectionFailureLogger logger) { @@ -664,8 +645,7 @@ private static TypeMappedAnnotation createIfPossible( } @SuppressWarnings("unchecked") - @Nullable - static Object extractFromMap(Method attribute, @Nullable Object map) { + static @Nullable Object extractFromMap(Method attribute, @Nullable Object map) { return (map != null ? ((Map) map).get(attribute.getName()) : null); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java index 7187cceb42ae..81b0b2c19260 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/TypeMappedAnnotations.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,7 +29,8 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; /** * {@link MergedAnnotations} implementation that searches for and adapts @@ -48,26 +49,21 @@ final class TypeMappedAnnotations implements MergedAnnotations { null, new Annotation[0], RepeatableContainers.none(), AnnotationFilter.ALL); - @Nullable - private final Object source; + private final @Nullable Object source; - @Nullable - private final AnnotatedElement element; + private final @Nullable AnnotatedElement element; - @Nullable - private final SearchStrategy searchStrategy; + private final @Nullable SearchStrategy searchStrategy; private final Predicate> searchEnclosingClass; - @Nullable - private final Annotation[] annotations; + private final Annotation @Nullable [] annotations; private final RepeatableContainers repeatableContainers; private final AnnotationFilter annotationFilter; - @Nullable - private volatile List aggregates; + private volatile @Nullable List aggregates; private TypeMappedAnnotations(AnnotatedElement element, SearchStrategy searchStrategy, @@ -238,8 +234,7 @@ private List getAggregates() { return aggregates; } - @Nullable - private R scan(C criteria, AnnotationsProcessor processor) { + private @Nullable R scan(C criteria, AnnotationsProcessor processor) { if (this.annotations != null) { R result = processor.doWithAnnotations(criteria, 0, this.source, this.annotations); return processor.finish(result); @@ -314,9 +309,8 @@ private IsPresent(RepeatableContainers repeatableContainers, } @Override - @Nullable - public Boolean doWithAnnotations(Object requiredType, int aggregateIndex, - @Nullable Object source, Annotation[] annotations) { + public @Nullable Boolean doWithAnnotations(Object requiredType, int aggregateIndex, + @Nullable Object source, @Nullable Annotation[] annotations) { for (Annotation annotation : annotations) { if (annotation != null) { @@ -374,13 +368,11 @@ private class MergedAnnotationFinder private final Object requiredType; - @Nullable - private final Predicate> predicate; + private final @Nullable Predicate> predicate; private final MergedAnnotationSelector selector; - @Nullable - private MergedAnnotation result; + private @Nullable MergedAnnotation result; MergedAnnotationFinder(Object requiredType, @Nullable Predicate> predicate, @Nullable MergedAnnotationSelector selector) { @@ -391,15 +383,13 @@ private class MergedAnnotationFinder } @Override - @Nullable - public MergedAnnotation doWithAggregate(Object context, int aggregateIndex) { + public @Nullable MergedAnnotation doWithAggregate(Object context, int aggregateIndex) { return this.result; } @Override - @Nullable - public MergedAnnotation doWithAnnotations(Object type, int aggregateIndex, - @Nullable Object source, Annotation[] annotations) { + public @Nullable MergedAnnotation doWithAnnotations(Object type, int aggregateIndex, + @Nullable Object source, @Nullable Annotation[] annotations) { for (Annotation annotation : annotations) { if (annotation != null && !annotationFilter.matches(annotation)) { @@ -412,8 +402,7 @@ public MergedAnnotation doWithAnnotations(Object type, int aggregateIndex, return null; } - @Nullable - private MergedAnnotation process( + private @Nullable MergedAnnotation process( Object type, int aggregateIndex, @Nullable Object source, Annotation annotation) { Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); @@ -447,8 +436,7 @@ private void updateLastResult(MergedAnnotation candidate) { } @Override - @Nullable - public MergedAnnotation finish(@Nullable MergedAnnotation result) { + public @Nullable MergedAnnotation finish(@Nullable MergedAnnotation result) { return (result != null ? result : this.result); } } @@ -462,26 +450,25 @@ private class AggregatesCollector implements AnnotationsProcessor aggregates = new ArrayList<>(); @Override - @Nullable - public List doWithAnnotations(Object criteria, int aggregateIndex, - @Nullable Object source, Annotation[] annotations) { + public @Nullable List doWithAnnotations(Object criteria, int aggregateIndex, + @Nullable Object source, @Nullable Annotation[] annotations) { this.aggregates.add(createAggregate(aggregateIndex, source, annotations)); return null; } - private Aggregate createAggregate(int aggregateIndex, @Nullable Object source, Annotation[] annotations) { + private Aggregate createAggregate(int aggregateIndex, @Nullable Object source, @Nullable Annotation[] annotations) { List aggregateAnnotations = getAggregateAnnotations(annotations); return new Aggregate(aggregateIndex, source, aggregateAnnotations); } - private List getAggregateAnnotations(Annotation[] annotations) { + private List getAggregateAnnotations(@Nullable Annotation[] annotations) { List result = new ArrayList<>(annotations.length); addAggregateAnnotations(result, annotations); return result; } - private void addAggregateAnnotations(List aggregateAnnotations, Annotation[] annotations) { + private void addAggregateAnnotations(List aggregateAnnotations, @Nullable Annotation[] annotations) { for (Annotation annotation : annotations) { if (annotation != null && !annotationFilter.matches(annotation)) { Annotation[] repeatedAnnotations = repeatableContainers.findRepeatedAnnotations(annotation); @@ -496,7 +483,7 @@ private void addAggregateAnnotations(List aggregateAnnotations, Anno } @Override - public List finish(@Nullable List processResult) { + public @NonNull List finish(@Nullable List processResult) { return this.aggregates; } } @@ -506,8 +493,7 @@ private static class Aggregate { private final int aggregateIndex; - @Nullable - private final Object source; + private final @Nullable Object source; private final List annotations; @@ -527,8 +513,7 @@ int size() { return this.annotations.size(); } - @Nullable - AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { + @Nullable AnnotationTypeMapping getMapping(int annotationIndex, int mappingIndex) { AnnotationTypeMappings mappings = getMappings(annotationIndex); return (mappingIndex < mappings.size() ? mappings.get(mappingIndex) : null); } @@ -537,8 +522,7 @@ AnnotationTypeMappings getMappings(int annotationIndex) { return this.mappings[annotationIndex]; } - @Nullable - MergedAnnotation createMergedAnnotationIfPossible( + @Nullable MergedAnnotation createMergedAnnotationIfPossible( int annotationIndex, int mappingIndex, IntrospectionFailureLogger logger) { return TypeMappedAnnotation.createIfPossible( @@ -554,15 +538,13 @@ MergedAnnotation createMergedAnnotationIfPossible( */ private class AggregatesSpliterator implements Spliterator> { - @Nullable - private final Object requiredType; + private final @Nullable Object requiredType; private final List aggregates; private int aggregateCursor; - @Nullable - private int[] mappingCursors; + private int @Nullable [] mappingCursors; AggregatesSpliterator(@Nullable Object requiredType, List aggregates) { this.requiredType = requiredType; @@ -613,8 +595,7 @@ private boolean tryAdvance(Aggregate aggregate, Consumer> trySplit() { + public @Nullable Spliterator> trySplit() { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java b/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java index 5f1b2146393c..e3cc58c27fef 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/ValueExtractor.java @@ -20,7 +20,7 @@ import java.lang.reflect.Method; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy API for extracting a value for an annotation attribute from a given @@ -37,7 +37,6 @@ interface ValueExtractor { * Extract the annotation attribute represented by the supplied {@link Method} * from the supplied source {@link Object}. */ - @Nullable - Object extract(Method attribute, @Nullable Object object); + @Nullable Object extract(Method attribute, @Nullable Object object); } diff --git a/spring-core/src/main/java/org/springframework/core/annotation/package-info.java b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java index af9d8e10ab11..27602936c7c2 100644 --- a/spring-core/src/main/java/org/springframework/core/annotation/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/annotation/package-info.java @@ -2,9 +2,7 @@ * Core support package for annotations, meta-annotations, and merged * annotations with attribute overrides. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java index 340792259bbd..7df7afe64a95 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractCharSequenceDecoder.java @@ -26,6 +26,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -35,7 +36,6 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.LimitedDataBufferList; import org.springframework.core.log.LogFormatUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; 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 655b53e41130..a26d106ff2c7 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. @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -25,7 +26,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** @@ -97,12 +97,11 @@ public Mono decodeToMono(Publisher input, ResolvableType elementT /** * 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 - @Nullable - protected T decodeDataBuffer(DataBuffer buffer, ResolvableType elementType, + @Deprecated(since = "5.2") + protected @Nullable T decodeDataBuffer(DataBuffer buffer, ResolvableType elementType, @Nullable MimeType mimeType, @Nullable Map hints) { return decode(buffer, elementType, mimeType, hints); diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java index 7810d20681e4..ddf3dbcf9c48 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractDecoder.java @@ -22,12 +22,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java index 49915e02a703..160be4b1f28c 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractEncoder.java @@ -21,9 +21,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java index 719b6f2773bb..74da39021665 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/AbstractSingleValueEncoder.java @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -25,7 +26,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java index 65f8222d1774..74aa33513471 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayDecoder.java @@ -18,10 +18,11 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java index 6eef1a1f771f..94329d6eef4c 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteArrayEncoder.java @@ -18,13 +18,13 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java index 0da03706e5bd..3f2659e46b3a 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferDecoder.java @@ -19,10 +19,11 @@ import java.nio.ByteBuffer; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java index 8d600661526b..c86371db6b36 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ByteBufferEncoder.java @@ -19,13 +19,13 @@ import java.nio.ByteBuffer; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java index 6745a2e00c61..0ce75215bb36 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/CharBufferDecoder.java @@ -21,9 +21,10 @@ import java.nio.charset.Charset; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java index 4103a1cec2c3..b7e239cfed07 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/CharSequenceEncoder.java @@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -31,7 +32,6 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.log.LogFormatUtils; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/CodecException.java b/spring-core/src/main/java/org/springframework/core/codec/CodecException.java index f621858783a7..a519d9d7233b 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/CodecException.java +++ b/spring-core/src/main/java/org/springframework/core/codec/CodecException.java @@ -16,8 +16,9 @@ package org.springframework.core.codec; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * General error that indicates a problem while encoding and decoding to and diff --git a/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java index 788eafdb0e69..c03508858f63 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/DataBufferDecoder.java @@ -18,12 +18,12 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java index 88e8f1cc2a96..4f7af8db2209 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/DataBufferEncoder.java @@ -18,13 +18,13 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java index f49063f101ae..285b46998b10 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Decoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Decoder.java @@ -22,13 +22,13 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -90,8 +90,7 @@ Mono decodeToMono(Publisher inputStream, ResolvableType elementTy * @return the decoded value, possibly {@code null} * @since 5.2 */ - @Nullable - default T decode(DataBuffer buffer, ResolvableType targetType, + default @Nullable T decode(DataBuffer buffer, ResolvableType targetType, @Nullable MimeType mimeType, @Nullable Map hints) throws DecodingException { CompletableFuture future = decodeToMono(Mono.just(buffer), targetType, mimeType, hints).toFuture(); diff --git a/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java b/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java index 873996dbcb36..d87a0aac10ba 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java +++ b/spring-core/src/main/java/org/springframework/core/codec/DecodingException.java @@ -16,7 +16,7 @@ package org.springframework.core.codec; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Indicates an issue with decoding the input stream with a focus on content diff --git a/spring-core/src/main/java/org/springframework/core/codec/Encoder.java b/spring-core/src/main/java/org/springframework/core/codec/Encoder.java index 8cbcfcfbc93b..551c6dd9d1dc 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Encoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Encoder.java @@ -20,6 +20,7 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,7 +28,6 @@ import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** diff --git a/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java b/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java index 7d9f7a1a1b12..5c5b83f22962 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java +++ b/spring-core/src/main/java/org/springframework/core/codec/EncodingException.java @@ -16,7 +16,7 @@ package org.springframework.core.codec; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Indicates an issue with encoding the input Object stream with a focus on diff --git a/spring-core/src/main/java/org/springframework/core/codec/Hints.java b/spring-core/src/main/java/org/springframework/core/codec/Hints.java index 21ad5dd20430..91b44b362558 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/Hints.java +++ b/spring-core/src/main/java/org/springframework/core/codec/Hints.java @@ -20,10 +20,10 @@ import java.util.Map; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/Netty5BufferDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/Netty5BufferDecoder.java deleted file mode 100644 index 150db2b893e6..000000000000 --- a/spring-core/src/main/java/org/springframework/core/codec/Netty5BufferDecoder.java +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.codec; - -import java.util.Map; - -import io.netty5.buffer.Buffer; -import io.netty5.buffer.DefaultBufferAllocators; - -import org.springframework.core.ResolvableType; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.core.io.buffer.Netty5DataBuffer; -import org.springframework.lang.Nullable; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; - -/** - * Decoder for {@link Buffer Buffers}. - * - * @author Violeta Georgieva - * @since 6.0 - */ -public class Netty5BufferDecoder extends AbstractDataBufferDecoder { - - public Netty5BufferDecoder() { - super(MimeTypeUtils.ALL); - } - - - @Override - public boolean canDecode(ResolvableType elementType, @Nullable MimeType mimeType) { - return (Buffer.class.isAssignableFrom(elementType.toClass()) && - super.canDecode(elementType, mimeType)); - } - - @Override - public Buffer decode(DataBuffer dataBuffer, ResolvableType elementType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - if (logger.isDebugEnabled()) { - logger.debug(Hints.getLogPrefix(hints) + "Read " + dataBuffer.readableByteCount() + " bytes"); - } - if (dataBuffer instanceof Netty5DataBuffer netty5DataBuffer) { - return netty5DataBuffer.getNativeBuffer(); - } - byte[] bytes = new byte[dataBuffer.readableByteCount()]; - dataBuffer.read(bytes); - Buffer buffer = DefaultBufferAllocators.preferredAllocator().copyOf(bytes); - DataBufferUtils.release(dataBuffer); - return buffer; - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/codec/Netty5BufferEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/Netty5BufferEncoder.java deleted file mode 100644 index 588367132d96..000000000000 --- a/spring-core/src/main/java/org/springframework/core/codec/Netty5BufferEncoder.java +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.codec; - -import java.util.Map; - -import io.netty5.buffer.Buffer; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Flux; - -import org.springframework.core.ResolvableType; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.io.buffer.DataBufferFactory; -import org.springframework.core.io.buffer.Netty5DataBufferFactory; -import org.springframework.lang.Nullable; -import org.springframework.util.MimeType; -import org.springframework.util.MimeTypeUtils; - -/** - * Encoder for {@link Buffer Buffers}. - * - * @author Violeta Georgieva - * @since 6.0 - */ -public class Netty5BufferEncoder extends AbstractEncoder { - - public Netty5BufferEncoder() { - super(MimeTypeUtils.ALL); - } - - - @Override - public boolean canEncode(ResolvableType type, @Nullable MimeType mimeType) { - Class clazz = type.toClass(); - return super.canEncode(type, mimeType) && Buffer.class.isAssignableFrom(clazz); - } - - @Override - public Flux encode(Publisher inputStream, - DataBufferFactory bufferFactory, ResolvableType elementType, @Nullable MimeType mimeType, - @Nullable Map hints) { - - return Flux.from(inputStream).map(byteBuffer -> - encodeValue(byteBuffer, bufferFactory, elementType, mimeType, hints)); - } - - @Override - public DataBuffer encodeValue(Buffer buffer, DataBufferFactory bufferFactory, ResolvableType valueType, - @Nullable MimeType mimeType, @Nullable Map hints) { - - if (logger.isDebugEnabled() && !Hints.isLoggingSuppressed(hints)) { - String logPrefix = Hints.getLogPrefix(hints); - logger.debug(logPrefix + "Writing " + buffer.readableBytes() + " bytes"); - } - if (bufferFactory instanceof Netty5DataBufferFactory netty5DataBufferFactory) { - return netty5DataBufferFactory.wrap(buffer); - } - byte[] bytes = new byte[buffer.readableBytes()]; - buffer.readBytes(bytes, 0, bytes.length); - buffer.close(); - return bufferFactory.wrap(bytes); - } -} diff --git a/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java index 569a5cc83246..7f185e6fc157 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufDecoder.java @@ -20,12 +20,12 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.Unpooled; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java index da07d638aa0e..ea177f3863ed 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/NettyByteBufEncoder.java @@ -19,6 +19,7 @@ import java.util.Map; import io.netty.buffer.ByteBuf; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -26,7 +27,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java index b62964ef36df..ab0ca020a418 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceDecoder.java @@ -19,6 +19,7 @@ import java.io.ByteArrayInputStream; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -28,7 +29,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -80,8 +80,7 @@ public Resource decode(DataBuffer dataBuffer, ResolvableType elementType, if (clazz == InputStreamResource.class) { return new InputStreamResource(new ByteArrayInputStream(bytes)) { @Override - @Nullable - public String getFilename() { + public @Nullable String getFilename() { return filename; } @Override @@ -93,8 +92,7 @@ public long contentLength() { else if (Resource.class.isAssignableFrom(clazz)) { return new ByteArrayResource(bytes) { @Override - @Nullable - public String getFilename() { + public @Nullable String getFilename() { return filename; } }; diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java index 58b0a094fbea..1c6c29a9f734 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceEncoder.java @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import org.springframework.core.ResolvableType; @@ -25,7 +26,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java b/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java index 3330ac6e1555..53997baf7761 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/ResourceRegionEncoder.java @@ -21,6 +21,7 @@ import java.util.Map; import java.util.OptionalLong; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -32,7 +33,6 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.support.ResourceRegion; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -71,8 +71,8 @@ public ResourceRegionEncoder(int bufferSize) { @Override public boolean canEncode(ResolvableType elementType, @Nullable MimeType mimeType) { - return super.canEncode(elementType, mimeType) - && ResourceRegion.class.isAssignableFrom(elementType.toClass()); + return super.canEncode(elementType, mimeType) && + ResourceRegion.class.isAssignableFrom(elementType.toClass()); } @Override diff --git a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java index d68abc2a33e3..2612e5b5988a 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java +++ b/spring-core/src/main/java/org/springframework/core/codec/StringDecoder.java @@ -19,9 +19,10 @@ import java.nio.charset.Charset; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-core/src/main/java/org/springframework/core/codec/package-info.java b/spring-core/src/main/java/org/springframework/core/codec/package-info.java index acf70ed8f96c..4fcd5b047a71 100644 --- a/spring-core/src/main/java/org/springframework/core/codec/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/codec/package-info.java @@ -3,9 +3,7 @@ * {@link org.springframework.core.codec.Decoder} abstractions to convert * between a reactive stream of bytes and Java objects. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.codec; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java index 9f47c8317578..7fb395086bb2 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionFailedException.java @@ -16,7 +16,8 @@ package org.springframework.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** @@ -29,13 +30,11 @@ @SuppressWarnings("serial") public class ConversionFailedException extends ConversionException { - @Nullable - private final TypeDescriptor sourceType; + private final @Nullable TypeDescriptor sourceType; private final TypeDescriptor targetType; - @Nullable - private final Object value; + private final @Nullable Object value; /** @@ -59,8 +58,7 @@ public ConversionFailedException(@Nullable TypeDescriptor sourceType, TypeDescri /** * Return the source type we tried to convert the value from. */ - @Nullable - public TypeDescriptor getSourceType() { + public @Nullable TypeDescriptor getSourceType() { return this.sourceType; } @@ -74,8 +72,7 @@ public TypeDescriptor getTargetType() { /** * Return the offending value. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java index 9c02d51cbcf4..3ce51475b11a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/ConversionService.java @@ -16,7 +16,7 @@ package org.springframework.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A service interface for type conversion. This is the entry point into the convert system. @@ -72,8 +72,7 @@ public interface ConversionService { * @throws ConversionException if a conversion exception occurred * @throws IllegalArgumentException if targetType is {@code null} */ - @Nullable - T convert(@Nullable Object source, Class targetType); + @Nullable T convert(@Nullable Object source, Class targetType); /** * Convert the given {@code source} to the specified {@code targetType}. @@ -87,8 +86,7 @@ public interface ConversionService { * @throws IllegalArgumentException if targetType is {@code null} * @since 6.1 */ - @Nullable - default Object convert(@Nullable Object source, TypeDescriptor targetType) { + default @Nullable Object convert(@Nullable Object source, TypeDescriptor targetType) { return convert(source, TypeDescriptor.forObject(source), targetType); } @@ -105,7 +103,6 @@ default Object convert(@Nullable Object source, TypeDescriptor targetType) { * @throws IllegalArgumentException if targetType is {@code null}, * or {@code sourceType} is {@code null} but source is not {@code null} */ - @Nullable - Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); + @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java b/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java index e4dd3c51a04a..3b7f64ebf4b2 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java +++ b/spring-core/src/main/java/org/springframework/core/convert/ConverterNotFoundException.java @@ -16,7 +16,7 @@ package org.springframework.core.convert; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception to be thrown when a suitable converter could not be found @@ -29,8 +29,7 @@ @SuppressWarnings("serial") public class ConverterNotFoundException extends ConversionException { - @Nullable - private final TypeDescriptor sourceType; + private final @Nullable TypeDescriptor sourceType; private final TypeDescriptor targetType; @@ -50,8 +49,7 @@ public ConverterNotFoundException(@Nullable TypeDescriptor sourceType, TypeDescr /** * Return the source type that was requested to convert from. */ - @Nullable - public TypeDescriptor getSourceType() { + public @Nullable TypeDescriptor getSourceType() { return this.sourceType; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/Property.java b/spring-core/src/main/java/org/springframework/core/convert/Property.java index f22251515084..2d1eed925252 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/Property.java +++ b/spring-core/src/main/java/org/springframework/core/convert/Property.java @@ -24,8 +24,9 @@ import java.util.Map; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -52,18 +53,15 @@ public final class Property { private final Class objectType; - @Nullable - private final Method readMethod; + private final @Nullable Method readMethod; - @Nullable - private final Method writeMethod; + private final @Nullable Method writeMethod; private final String name; private final MethodParameter methodParameter; - @Nullable - private Annotation[] annotations; + private Annotation @Nullable [] annotations; public Property(Class objectType, @Nullable Method readMethod, @Nullable Method writeMethod) { @@ -105,16 +103,14 @@ public Class getType() { /** * The property getter method: for example, {@code getFoo()}. */ - @Nullable - public Method getReadMethod() { + public @Nullable Method getReadMethod() { return this.readMethod; } /** * The property setter method: for example, {@code setFoo(String)}. */ - @Nullable - public Method getWriteMethod() { + public @Nullable Method getWriteMethod() { return this.writeMethod; } @@ -185,16 +181,14 @@ private MethodParameter resolveMethodParameter() { return write; } - @Nullable - private MethodParameter resolveReadMethodParameter() { + private @Nullable MethodParameter resolveReadMethodParameter() { if (getReadMethod() == null) { return null; } return new MethodParameter(getReadMethod(), -1).withContainingClass(getObjectType()); } - @Nullable - private MethodParameter resolveWriteMethodParameter() { + private @Nullable MethodParameter resolveWriteMethodParameter() { if (getWriteMethod() == null) { return null; } @@ -224,8 +218,7 @@ private void addAnnotationsToMap( } } - @Nullable - private Field getField() { + private @Nullable Field getField() { String name = getName(); if (!StringUtils.hasLength(name)) { return null; @@ -245,8 +238,7 @@ private Field getField() { return field; } - @Nullable - private Class declaringClass() { + private @Nullable Class declaringClass() { if (getReadMethod() != null) { return getReadMethod().getDeclaringClass(); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java index c98930f61263..d83a6fb3d4b1 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/convert/TypeDescriptor.java @@ -18,7 +18,6 @@ import java.io.Serializable; import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Type; import java.util.Arrays; @@ -28,16 +27,19 @@ import java.util.function.Supplier; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; +import org.springframework.core.annotation.AnnotatedElementAdapter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; /** * Contextual descriptor about a type to convert from or to. + * *

    Capable of representing arrays and generic collection types. * * @author Keith Donald @@ -73,8 +75,7 @@ public class TypeDescriptor implements Serializable { private final AnnotatedElementSupplier annotatedElementSupplier; - @Nullable - private volatile AnnotatedElementAdapter annotatedElement; + private volatile @Nullable AnnotatedElementAdapter annotatedElement; /** @@ -124,7 +125,7 @@ public TypeDescriptor(Property property) { * @param annotations the type annotations * @since 4.0 */ - public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, @Nullable Annotation[] annotations) { + public TypeDescriptor(ResolvableType resolvableType, @Nullable Class type, Annotation @Nullable [] annotations) { this.resolvableType = resolvableType; this.type = (type != null ? type : resolvableType.toClass()); this.annotatedElementSupplier = () -> AnnotatedElementAdapter.from(annotations); @@ -181,8 +182,7 @@ public Object getSource() { * {@code null} if it could not be obtained * @since 6.1 */ - @Nullable - public TypeDescriptor nested(int nestingLevel) { + public @Nullable TypeDescriptor nested(int nestingLevel) { ResolvableType nested = this.resolvableType; for (int i = 0; i < nestingLevel; i++) { if (Object.class == nested.getType()) { @@ -231,8 +231,7 @@ public TypeDescriptor narrow(@Nullable Object value) { * @throws IllegalArgumentException if this type is not assignable to the super-type * @since 3.2 */ - @Nullable - public TypeDescriptor upcast(@Nullable Class superType) { + public @Nullable TypeDescriptor upcast(@Nullable Class superType) { if (superType == null) { return null; } @@ -294,8 +293,7 @@ public boolean hasAnnotation(Class annotationType) { * @param annotationType the annotation type * @return the annotation, or {@code null} if no such annotation exists on this type descriptor */ - @Nullable - public T getAnnotation(Class annotationType) { + public @Nullable T getAnnotation(Class annotationType) { AnnotatedElementAdapter annotatedElement = getAnnotatedElement(); if (annotatedElement.isEmpty()) { // Shortcut: AnnotatedElementUtils would have to expect AnnotatedElement.getAnnotations() @@ -369,8 +367,7 @@ public boolean isArray() { * an array type or a {@code java.util.Collection} or if its element type is not parameterized * @see #elementTypeDescriptor(Object) */ - @Nullable - public TypeDescriptor getElementTypeDescriptor() { + public @Nullable TypeDescriptor getElementTypeDescriptor() { if (getResolvableType().isArray()) { return new TypeDescriptor(getResolvableType().getComponentType(), null, getAnnotations()); } @@ -397,8 +394,7 @@ public TypeDescriptor getElementTypeDescriptor() { * @see #getElementTypeDescriptor() * @see #narrow(Object) */ - @Nullable - public TypeDescriptor elementTypeDescriptor(Object element) { + public @Nullable TypeDescriptor elementTypeDescriptor(Object element) { return narrow(element, getElementTypeDescriptor()); } @@ -417,8 +413,7 @@ public boolean isMap() { * but its key type is not parameterized * @throws IllegalStateException if this type is not a {@code java.util.Map} */ - @Nullable - public TypeDescriptor getMapKeyTypeDescriptor() { + public @Nullable TypeDescriptor getMapKeyTypeDescriptor() { Assert.state(isMap(), "Not a [java.util.Map]"); return getRelatedIfResolvable(getResolvableType().asMap().getGeneric(0)); } @@ -440,8 +435,7 @@ public TypeDescriptor getMapKeyTypeDescriptor() { * @throws IllegalStateException if this type is not a {@code java.util.Map} * @see #narrow(Object) */ - @Nullable - public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) { + public @Nullable TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) { return narrow(mapKey, getMapKeyTypeDescriptor()); } @@ -454,8 +448,7 @@ public TypeDescriptor getMapKeyTypeDescriptor(Object mapKey) { * but its value type is not parameterized * @throws IllegalStateException if this type is not a {@code java.util.Map} */ - @Nullable - public TypeDescriptor getMapValueTypeDescriptor() { + public @Nullable TypeDescriptor getMapValueTypeDescriptor() { Assert.state(isMap(), "Not a [java.util.Map]"); return getRelatedIfResolvable(getResolvableType().asMap().getGeneric(1)); } @@ -477,21 +470,18 @@ public TypeDescriptor getMapValueTypeDescriptor() { * @throws IllegalStateException if this type is not a {@code java.util.Map} * @see #narrow(Object) */ - @Nullable - public TypeDescriptor getMapValueTypeDescriptor(@Nullable Object mapValue) { + public @Nullable TypeDescriptor getMapValueTypeDescriptor(@Nullable Object mapValue) { return narrow(mapValue, getMapValueTypeDescriptor()); } - @Nullable - private TypeDescriptor getRelatedIfResolvable(ResolvableType type) { + private @Nullable TypeDescriptor getRelatedIfResolvable(ResolvableType type) { if (type.resolve() == null) { return null; } return new TypeDescriptor(type, null, getAnnotations()); } - @Nullable - private TypeDescriptor narrow(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) { + private @Nullable TypeDescriptor narrow(@Nullable Object value, @Nullable TypeDescriptor typeDescriptor) { if (typeDescriptor != null) { return typeDescriptor.narrow(value); } @@ -567,9 +557,8 @@ public String toString() { * @param source the source object * @return the type descriptor */ - @Nullable @Contract("!null -> !null; null -> null") - public static TypeDescriptor forObject(@Nullable Object source) { + public static @Nullable TypeDescriptor forObject(@Nullable Object source) { return (source != null ? valueOf(source.getClass()) : null); } @@ -648,9 +637,8 @@ public static TypeDescriptor map(Class mapType, @Nullable TypeDescriptor keyT * @return an array {@link TypeDescriptor} or {@code null} if {@code elementTypeDescriptor} is {@code null} * @since 3.2.1 */ - @Nullable @Contract("!null -> !null; null -> null") - public static TypeDescriptor array(@Nullable TypeDescriptor elementTypeDescriptor) { + public static @Nullable TypeDescriptor array(@Nullable TypeDescriptor elementTypeDescriptor) { if (elementTypeDescriptor == null) { return null; } @@ -680,8 +668,7 @@ public static TypeDescriptor array(@Nullable TypeDescriptor elementTypeDescripto * {@link MethodParameter} argument is not 1, or if the types up to the * specified nesting level are not of collection, array, or map types */ - @Nullable - public static TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { + public static @Nullable TypeDescriptor nested(MethodParameter methodParameter, int nestingLevel) { if (methodParameter.getNestingLevel() != 1) { throw new IllegalArgumentException("MethodParameter nesting level must be 1: " + "use the nestingLevel parameter to specify the desired nestingLevel for nested type traversal"); @@ -710,8 +697,7 @@ public static TypeDescriptor nested(MethodParameter methodParameter, int nesting * @throws IllegalArgumentException if the types up to the specified nesting * level are not of collection, array, or map types */ - @Nullable - public static TypeDescriptor nested(Field field, int nestingLevel) { + public static @Nullable TypeDescriptor nested(Field field, int nestingLevel) { return new TypeDescriptor(field).nested(nestingLevel); } @@ -736,8 +722,7 @@ public static TypeDescriptor nested(Field field, int nestingLevel) { * @throws IllegalArgumentException if the types up to the specified nesting * level are not of collection, array, or map types */ - @Nullable - public static TypeDescriptor nested(Property property, int nestingLevel) { + public static @Nullable TypeDescriptor nested(Property property, int nestingLevel) { return new TypeDescriptor(property).nested(nestingLevel); } @@ -747,83 +732,6 @@ private static String getName(Class clazz) { } - /** - * Adapter class for exposing a {@code TypeDescriptor}'s annotations as an - * {@link AnnotatedElement}, in particular to {@link AnnotatedElementUtils}. - * @see AnnotatedElementUtils#isAnnotated(AnnotatedElement, Class) - * @see AnnotatedElementUtils#getMergedAnnotation(AnnotatedElement, Class) - */ - private static final class AnnotatedElementAdapter implements AnnotatedElement, Serializable { - - private static final AnnotatedElementAdapter EMPTY = new AnnotatedElementAdapter(new Annotation[0]); - - private final Annotation[] annotations; - - private AnnotatedElementAdapter(Annotation[] annotations) { - this.annotations = annotations; - } - - private static AnnotatedElementAdapter from(@Nullable Annotation[] annotations) { - if (annotations == null || annotations.length == 0) { - return EMPTY; - } - return new AnnotatedElementAdapter(annotations); - } - - @Override - public boolean isAnnotationPresent(Class annotationClass) { - for (Annotation annotation : this.annotations) { - if (annotation.annotationType() == annotationClass) { - return true; - } - } - return false; - } - - @Override - @Nullable - @SuppressWarnings("unchecked") - public T getAnnotation(Class annotationClass) { - for (Annotation annotation : this.annotations) { - if (annotation.annotationType() == annotationClass) { - return (T) annotation; - } - } - return null; - } - - @Override - public Annotation[] getAnnotations() { - return (isEmpty() ? this.annotations : this.annotations.clone()); - } - - @Override - public Annotation[] getDeclaredAnnotations() { - return getAnnotations(); - } - - public boolean isEmpty() { - return (this.annotations.length == 0); - } - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof AnnotatedElementAdapter that && - Arrays.equals(this.annotations, that.annotations))); - } - - @Override - public int hashCode() { - return Arrays.hashCode(this.annotations); - } - - @Override - public String toString() { - return Arrays.toString(this.annotations); - } - } - - private interface AnnotatedElementSupplier extends Supplier, Serializable { } diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java index b603fdc28bbe..5f3b59b37e5a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/Converter.java @@ -16,7 +16,8 @@ package org.springframework.core.convert.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -41,8 +42,7 @@ public interface Converter { * @return the converted object, which must be an instance of {@code T} (potentially {@code null}) * @throws IllegalArgumentException if the source cannot be converted to the desired target type */ - @Nullable - T convert(S source); + @Nullable T convert(S source); /** * Construct a composed {@link Converter} that first applies this {@link Converter} diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java b/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java index a313a7938a1d..63b122550b03 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/ConvertingComparator.java @@ -19,8 +19,9 @@ import java.util.Comparator; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.comparator.Comparators; @@ -120,8 +121,7 @@ public ConversionServiceConverter(ConversionService conversionService, ClassFor {@link ConditionalConverter conditional converters} this method may return * {@code null} to indicate all source-to-target pairs should be considered. */ - @Nullable - Set getConvertibleTypes(); + @Nullable Set getConvertibleTypes(); /** * Convert the source object to the targetType described by the {@code TypeDescriptor}. @@ -63,8 +63,7 @@ public interface GenericConverter { * @param targetType the type descriptor of the field we are converting to * @return the converted object */ - @Nullable - Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType); + @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType); /** diff --git a/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java index c7a057daabdf..9438b49ce72f 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/convert/converter/package-info.java @@ -1,9 +1,7 @@ /** * SPI to implement Converters for the type conversion system. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.convert.converter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/convert/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/package-info.java index 7cef7265ea03..2c0dbc6cb3c1 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/convert/package-info.java @@ -1,9 +1,7 @@ /** * Type conversion system API. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.convert; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java index 10307745227a..0326aaab2f00 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToArrayConverter.java @@ -21,10 +21,11 @@ import java.util.List; 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; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -61,8 +62,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (this.conversionService instanceof GenericConversionService genericConversionService) { TypeDescriptor targetElement = targetType.getElementTypeDescriptor(); if (targetElement != null && targetType.getType().isInstance(source) && diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java index 7a9c76239da9..01f9e0626e4f 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToCollectionConverter.java @@ -22,11 +22,12 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Converts an array to a Collection. @@ -62,8 +63,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java index 4547d1873451..40e58ca3ffa1 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToObjectConverter.java @@ -20,10 +20,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; /** * Converts an array to an Object by returning the first array element @@ -53,8 +54,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java index fad85e51337f..5f5e4845643f 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ArrayToStringConverter.java @@ -20,10 +20,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -55,8 +56,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return this.helperConverter.convert(Arrays.asList(ObjectUtils.toObjectArray(source)), sourceType, targetType); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java index 3023eb6781fa..55c7791f12a0 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ByteBufferConverter.java @@ -19,10 +19,11 @@ import java.nio.ByteBuffer; 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; -import org.springframework.lang.Nullable; /** * Converts a {@link ByteBuffer} directly to and from {@code byte[] ByteBuffer} directly to and from {@code byte[]s} and indirectly @@ -77,8 +78,7 @@ private boolean matchesToByteBuffer(TypeDescriptor sourceType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { boolean byteBufferTarget = targetType.isAssignableTo(BYTE_BUFFER_TYPE); if (source instanceof ByteBuffer buffer) { return (byteBufferTarget ? buffer.duplicate() : convertFromByteBuffer(buffer, targetType)); @@ -90,8 +90,7 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe throw new IllegalStateException("Unexpected source/target types"); } - @Nullable - private Object convertFromByteBuffer(ByteBuffer source, TypeDescriptor targetType) { + private @Nullable Object convertFromByteBuffer(ByteBuffer source, TypeDescriptor targetType) { byte[] bytes = new byte[source.remaining()]; source.get(bytes); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java index 026587a03d66..83fd24e26046 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToArrayConverter.java @@ -21,10 +21,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,8 +62,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java index f20c950d61e1..3447c8a728ea 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToCollectionConverter.java @@ -20,11 +20,12 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Converts from a Collection to another Collection. @@ -60,8 +61,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java index 81b4a41adcff..23433009be35 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToObjectConverter.java @@ -20,10 +20,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; /** * Converts a Collection to an Object by returning the first collection element after converting it to the desired targetType. @@ -50,8 +51,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java index e007ee279dfa..e1d7313c1bc3 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/CollectionToStringConverter.java @@ -21,10 +21,11 @@ import java.util.Set; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Converts a Collection to a comma-delimited String. @@ -56,8 +57,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (!(source instanceof Collection sourceCollection)) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java index 7275d3124124..901fb2215f51 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionServiceFactory.java @@ -18,11 +18,12 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.lang.Nullable; /** * A factory for common {@link org.springframework.core.convert.ConversionService} diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java index f2f400da324d..aad0e4d13bc9 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConversionUtils.java @@ -16,11 +16,12 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.GenericConverter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -33,8 +34,7 @@ */ abstract class ConversionUtils { - @Nullable - public static Object invokeConverter(GenericConverter converter, @Nullable Object source, + public static @Nullable Object invokeConverter(GenericConverter converter, @Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { try { diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java index dcf4c8bb8b14..c3787dc281a1 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ConvertingPropertyEditorAdapter.java @@ -18,9 +18,10 @@ import java.beans.PropertyEditorSupport; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,8 +62,7 @@ public void setAsText(@Nullable String text) throws IllegalArgumentException { } @Override - @Nullable - public String getAsText() { + public @Nullable String getAsText() { if (this.canConvertToString) { return (String) this.conversionService.convert(getValue(), this.targetDescriptor, TypeDescriptor.valueOf(String.class)); } 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 7e5acd9d1858..d7881accdcaa 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. @@ -22,10 +22,11 @@ import java.util.UUID; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + import org.springframework.core.KotlinDetector; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.converter.ConverterRegistry; -import org.springframework.lang.Nullable; /** * A specialization of {@link GenericConversionService} configured by default @@ -42,8 +43,7 @@ */ public class DefaultConversionService extends GenericConversionService { - @Nullable - private static volatile DefaultConversionService sharedInstance; + private static volatile @Nullable DefaultConversionService sharedInstance; /** @@ -99,6 +99,7 @@ public static void addDefaultConverters(ConverterRegistry converterRegistry) { 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/FallbackObjectToStringConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java index 63313d3ae6a7..7f783a32f761 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/FallbackObjectToStringConverter.java @@ -20,9 +20,10 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Simply calls {@link Object#toString()} to convert any supported object @@ -61,8 +62,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return (source != null ? source.toString() : null); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java index 6a8b3f6bdebc..f4c960b923a1 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/GenericConversionService.java @@ -28,6 +28,8 @@ import java.util.concurrent.ConcurrentLinkedDeque; import java.util.concurrent.CopyOnWriteArraySet; +import org.jspecify.annotations.Nullable; + import org.springframework.core.DecoratingProxy; import org.springframework.core.ResolvableType; import org.springframework.core.convert.ConversionFailedException; @@ -41,7 +43,6 @@ import org.springframework.core.convert.converter.ConverterRegistry; import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.convert.converter.GenericConverter.ConvertiblePair; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -159,15 +160,13 @@ public boolean canBypassConvert(@Nullable TypeDescriptor sourceType, TypeDescrip @SuppressWarnings("unchecked") @Override - @Nullable - public T convert(@Nullable Object source, Class targetType) { + public @Nullable T convert(@Nullable Object source, Class targetType) { Assert.notNull(targetType, "Target type to convert to cannot be null"); return (T) convert(source, TypeDescriptor.forObject(source), TypeDescriptor.valueOf(targetType)); } @Override - @Nullable - public Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { Assert.notNull(targetType, "Target type to convert to cannot be null"); if (sourceType == null) { Assert.isTrue(source == null, "Source must be [null] if source type == [null]"); @@ -203,8 +202,7 @@ public String toString() { * @param targetType the target type to convert to * @return the converted null object */ - @Nullable - protected Object convertNullSource(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + protected @Nullable Object convertNullSource(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { if (targetType.getObjectType() == Optional.class) { return Optional.empty(); } @@ -222,8 +220,7 @@ protected Object convertNullSource(@Nullable TypeDescriptor sourceType, TypeDesc * or {@code null} if no suitable converter was found * @see #getDefaultConverter(TypeDescriptor, TypeDescriptor) */ - @Nullable - protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + protected @Nullable GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { ConverterCacheKey key = new ConverterCacheKey(sourceType, targetType); GenericConverter converter = this.converterCache.get(key); if (converter != null) { @@ -252,16 +249,14 @@ protected GenericConverter getConverter(TypeDescriptor sourceType, TypeDescripto * @param targetType the target type to convert to * @return the default generic converter that will perform the conversion */ - @Nullable - protected GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + protected @Nullable GenericConverter getDefaultConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { return (sourceType.isAssignableTo(targetType) ? NO_OP_CONVERTER : null); } // Internal helpers - @Nullable - private ResolvableType[] getRequiredTypeInfo(Class converterClass, Class genericIfc) { + private ResolvableType @Nullable [] getRequiredTypeInfo(Class converterClass, Class genericIfc) { ResolvableType resolvableType = ResolvableType.forClass(converterClass).as(genericIfc); ResolvableType[] generics = resolvableType.getGenerics(); if (generics.length < 2) { @@ -279,8 +274,7 @@ private void invalidateCache() { this.converterCache.clear(); } - @Nullable - private Object handleConverterNotFound( + private @Nullable Object handleConverterNotFound( @Nullable Object source, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { @@ -294,8 +288,7 @@ private Object handleConverterNotFound( throw new ConverterNotFoundException(sourceType, targetType); } - @Nullable - private Object handleResult(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType, @Nullable Object result) { + private @Nullable Object handleResult(@Nullable TypeDescriptor sourceType, TypeDescriptor targetType, @Nullable Object result) { if (result == null) { assertNotPrimitiveTargetType(sourceType, targetType); } @@ -356,8 +349,7 @@ public boolean matchesFallback(TypeDescriptor sourceType, TypeDescriptor targetT } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return convertNullSource(sourceType, targetType); } @@ -407,8 +399,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return convertNullSource(sourceType, targetType); } @@ -505,8 +496,7 @@ public void remove(Class sourceType, Class targetType) { * @param targetType the target type * @return a matching {@link GenericConverter}, or {@code null} if none found */ - @Nullable - public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetType) { // Search the full type hierarchy List> sourceCandidates = getClassHierarchy(sourceType.getType()); List> targetCandidates = getClassHierarchy(targetType.getType()); @@ -522,8 +512,7 @@ public GenericConverter find(TypeDescriptor sourceType, TypeDescriptor targetTyp return null; } - @Nullable - private GenericConverter getRegisteredConverter(TypeDescriptor sourceType, + private @Nullable GenericConverter getRegisteredConverter(TypeDescriptor sourceType, TypeDescriptor targetType, ConvertiblePair convertiblePair) { // Check specifically registered converters @@ -627,8 +616,7 @@ public void add(GenericConverter converter) { this.converters.addFirst(converter); } - @Nullable - public GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable GenericConverter getConverter(TypeDescriptor sourceType, TypeDescriptor targetType) { // Look for proper match among all converters (taking full generics into account) for (GenericConverter converter : this.converters) { if (!(converter instanceof ConditionalGenericConverter genericConverter) || @@ -665,14 +653,12 @@ public NoOpConverter(String name) { } @Override - @Nullable - public Set getConvertibleTypes() { + public @Nullable Set getConvertibleTypes() { return null; } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return source; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java index c3984fb039ad..7908ed6e579a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/IdToEntityConverter.java @@ -21,10 +21,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -63,8 +64,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } @@ -75,8 +75,7 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe return ReflectionUtils.invokeMethod(finder, source, id); } - @Nullable - private Method getFinder(Class entityClass) { + private @Nullable Method getFinder(Class entityClass) { String finderMethod = "find" + getEntityName(entityClass); Method[] methods; boolean localOnlyFiltered; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java index 3247e5bf3d0a..7f6fb0351eb4 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/MapToMapConverter.java @@ -22,11 +22,12 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Converts a Map to another Map. @@ -61,8 +62,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } @@ -114,16 +114,14 @@ private boolean canConvertValue(TypeDescriptor sourceType, TypeDescriptor target targetType.getMapValueTypeDescriptor(), this.conversionService); } - @Nullable - private Object convertKey(Object sourceKey, TypeDescriptor sourceType, @Nullable TypeDescriptor targetType) { + private @Nullable Object convertKey(Object sourceKey, TypeDescriptor sourceType, @Nullable TypeDescriptor targetType) { if (targetType == null) { return sourceKey; } return this.conversionService.convert(sourceKey, sourceType.getMapKeyTypeDescriptor(sourceKey), targetType); } - @Nullable - private Object convertValue(Object sourceValue, TypeDescriptor sourceType, @Nullable TypeDescriptor targetType) { + private @Nullable Object convertValue(Object sourceValue, TypeDescriptor sourceType, @Nullable TypeDescriptor targetType) { if (targetType == null) { return sourceValue; } @@ -133,11 +131,9 @@ private Object convertValue(Object sourceValue, TypeDescriptor sourceType, @Null private static class MapEntry { - @Nullable - private final Object key; + private final @Nullable Object key; - @Nullable - private final Object value; + private final @Nullable Object value; public MapEntry(@Nullable Object key, @Nullable Object value) { this.key = key; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java index e57a7b815329..21041df78470 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToArrayConverter.java @@ -20,10 +20,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -56,8 +57,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java index 28cf07c2b490..8d0e49f9ea89 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToCollectionConverter.java @@ -20,11 +20,12 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Converts an Object to a single-element Collection containing the Object. @@ -55,8 +56,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java index b65f11f24f0a..6d948c69270c 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/ObjectToObjectConverter.java @@ -25,10 +25,11 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionFailedException; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; @@ -89,8 +90,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } @@ -133,8 +133,7 @@ static boolean hasConversionMethodOrConstructor(Class targetClass, Class s return (getValidatedExecutable(targetClass, sourceClass) != null); } - @Nullable - private static Executable getValidatedExecutable(Class targetClass, Class sourceClass) { + private static @Nullable Executable getValidatedExecutable(Class targetClass, Class sourceClass) { Executable executable = conversionExecutableCache.get(targetClass); if (executable != null && isApplicable(executable, sourceClass)) { return executable; @@ -169,8 +168,7 @@ else if (executable instanceof Constructor constructor) { } } - @Nullable - private static Method determineToMethod(Class targetClass, Class sourceClass) { + private static @Nullable Method determineToMethod(Class targetClass, Class sourceClass) { if (String.class == targetClass || String.class == sourceClass) { // Do not accept a toString() method or any to methods on String itself return null; @@ -181,8 +179,7 @@ private static Method determineToMethod(Class targetClass, Class sourceCla ClassUtils.isAssignable(targetClass, method.getReturnType()) ? method : null); } - @Nullable - private static Method determineFactoryMethod(Class targetClass, Class sourceClass) { + private static @Nullable Method determineFactoryMethod(Class targetClass, Class sourceClass) { if (String.class == targetClass) { // Do not accept the String.valueOf(Object) method return null; @@ -209,8 +206,7 @@ private static boolean areRelatedTypes(Class type1, Class type2) { return (ClassUtils.isAssignable(type1, type2) || ClassUtils.isAssignable(type2, type1)); } - @Nullable - private static Constructor determineFactoryConstructor(Class targetClass, Class sourceClass) { + private static @Nullable Constructor determineFactoryConstructor(Class targetClass, Class sourceClass) { return ClassUtils.getConstructorIfAvailable(targetClass, sourceClass); } 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 2993326c4c7e..3cd9adf8779a 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. @@ -21,20 +21,22 @@ 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; -import org.springframework.lang.Nullable; 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 000000000000..7393473d81e5 --- /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/convert/support/StreamConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StreamConverter.java index 438d58c6f538..d1840e357e03 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StreamConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StreamConverter.java @@ -24,10 +24,11 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; /** * Converts a {@link Stream} to and from a collection or array, converting the @@ -89,8 +90,7 @@ public boolean matchesToStream(@Nullable TypeDescriptor elementType, TypeDescrip } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (sourceType.isAssignableTo(STREAM_TYPE)) { return convertFromStream((Stream) source, sourceType, targetType); } @@ -101,8 +101,7 @@ public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDe throw new IllegalStateException("Unexpected source/target types"); } - @Nullable - private Object convertFromStream(@Nullable Stream source, TypeDescriptor streamType, TypeDescriptor targetType) { + private @Nullable Object convertFromStream(@Nullable Stream source, TypeDescriptor streamType, TypeDescriptor targetType) { List content = (source != null ? source.collect(Collectors.toList()) : Collections.emptyList()); TypeDescriptor listType = TypeDescriptor.collection(List.class, streamType.getElementTypeDescriptor()); return this.conversionService.convert(content, listType, targetType); diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java index 7cece80cf880..7c6508c39b12 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToArrayConverter.java @@ -20,10 +20,11 @@ import java.util.Collections; 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; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -57,8 +58,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java index 694e85b87683..3f265186657d 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToBooleanConverter.java @@ -19,8 +19,9 @@ import java.util.Locale; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; /** * Converts a String to a Boolean. @@ -38,8 +39,7 @@ final class StringToBooleanConverter implements Converter { @Override - @Nullable - public Boolean convert(String source) { + public @Nullable Boolean convert(String source) { String value = source.trim(); if (value.isEmpty()) { return null; diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java index 97374fbba74f..92d987d7f758 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCharacterConverter.java @@ -16,8 +16,9 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; /** * Converts a String to a Character. @@ -28,8 +29,7 @@ final class StringToCharacterConverter implements Converter { @Override - @Nullable - public Character convert(String source) { + public @Nullable Character convert(String source) { if (source.isEmpty()) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java index df565b4ed1d3..4a7276e67fff 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToCollectionConverter.java @@ -20,11 +20,12 @@ import java.util.Collections; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.CollectionFactory; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.converter.ConditionalGenericConverter; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -58,8 +59,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { if (source == null) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java index 887690be5928..78427e4ca879 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToEnumConverterFactory.java @@ -16,9 +16,10 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; -import org.springframework.lang.Nullable; /** * Converts from a String to a {@link java.lang.Enum} by calling {@link Enum#valueOf(Class, String)}. @@ -45,8 +46,7 @@ private static class StringToEnum implements Converter { @Override - @Nullable - public Locale convert(String source) { + public @Nullable Locale convert(String source) { return StringUtils.parseLocale(source); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java index 082d4c972a6d..508741d5e7bd 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToNumberConverterFactory.java @@ -16,9 +16,10 @@ package org.springframework.core.convert.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.convert.converter.ConverterFactory; -import org.springframework.lang.Nullable; import org.springframework.util.NumberUtils; /** @@ -56,8 +57,7 @@ public StringToNumber(Class targetType) { } @Override - @Nullable - public T convert(String source) { + public @Nullable T convert(String source) { if (source.isEmpty()) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java index ca2634b2117b..512401f2436a 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToPatternConverter.java @@ -18,8 +18,9 @@ import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; /** * Converts from a String to a {@link java.util.regex.Pattern}. @@ -31,8 +32,7 @@ final class StringToPatternConverter implements Converter { @Override - @Nullable - public Pattern convert(String source) { + public @Nullable Pattern convert(String source) { if (source.isEmpty()) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java index 10a1b93d1981..35c75d23ccd3 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToRegexConverter.java @@ -17,9 +17,9 @@ package org.springframework.core.convert.support; import kotlin.text.Regex; +import org.jspecify.annotations.Nullable; import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; /** * Converts from a String to a Kotlin {@link Regex}. @@ -31,8 +31,7 @@ final class StringToRegexConverter implements Converter { @Override - @Nullable - public Regex convert(String source) { + public @Nullable Regex convert(String source) { if (source.isEmpty()) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java b/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java index cb63290f4eea..ea189065fb36 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/StringToUUIDConverter.java @@ -18,8 +18,9 @@ import java.util.UUID; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -32,8 +33,7 @@ final class StringToUUIDConverter implements Converter { @Override - @Nullable - public UUID convert(String source) { + public @Nullable UUID convert(String source) { return (StringUtils.hasText(source) ? UUID.fromString(source.trim()) : null); } diff --git a/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java b/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java index c516a7e37fd3..6ca951f898bd 100644 --- a/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/convert/support/package-info.java @@ -1,9 +1,7 @@ /** * Default implementation of the type conversion system. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.convert.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 de1e84c535b3..21a2d528cffc 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. @@ -24,10 +24,10 @@ 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.support.ConfigurableConversionService; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -42,7 +42,7 @@ * 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 @@ protected AbstractEnvironment(MutablePropertySources propertySources) { /** * 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 @@ protected ConfigurablePropertyResolver createPropertyResolver(MutablePropertySou } /** - * 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) */ @@ -288,8 +287,7 @@ protected Set doGetActiveProfiles() { * @since 5.3.4 * @see #ACTIVE_PROFILES_PROPERTY_NAME */ - @Nullable - protected String doGetActiveProfilesProperty() { + protected @Nullable String doGetActiveProfilesProperty() { return getProperty(ACTIVE_PROFILES_PROPERTY_NAME); } @@ -320,7 +318,6 @@ public void addActiveProfile(String profile) { } } - @Override public String[] getDefaultProfiles() { return StringUtils.toStringArray(doGetDefaultProfiles()); @@ -328,7 +325,7 @@ public String[] getDefaultProfiles() { /** * 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. @@ -355,8 +352,7 @@ protected Set doGetDefaultProfiles() { * @since 5.3.4 * @see #DEFAULT_PROFILES_PROPERTY_NAME */ - @Nullable - protected String doGetDefaultProfilesProperty() { + protected @Nullable String doGetDefaultProfilesProperty() { return getProperty(DEFAULT_PROFILES_PROPERTY_NAME); } @@ -381,7 +377,7 @@ public void setDefaultProfiles(String... profiles) { } @Override - @Deprecated + @Deprecated(since = "5.1") public boolean acceptsProfiles(String... profiles) { Assert.notEmpty(profiles, "Must specify at least one profile"); for (String profile : profiles) { @@ -420,7 +416,7 @@ protected boolean isProfileActive(String profile) { * 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 @@ -552,8 +548,7 @@ public boolean containsProperty(String key) { } @Override - @Nullable - public String getProperty(String key) { + public @Nullable String getProperty(String key) { return this.propertyResolver.getProperty(key); } @@ -563,8 +558,7 @@ public String getProperty(String key, String defaultValue) { } @Override - @Nullable - public T getProperty(String key, Class targetType) { + public @Nullable T getProperty(String key, Class targetType) { return this.propertyResolver.getProperty(key, targetType); } 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 151c482b7ed9..a228ab766e15 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. @@ -22,11 +22,12 @@ 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; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.PropertyPlaceholderHelper; @@ -37,20 +38,58 @@ * * @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()); - @Nullable - private volatile ConfigurableConversionService conversionService; + private volatile @Nullable ConfigurableConversionService conversionService; - @Nullable - private PropertyPlaceholderHelper nonStrictHelper; + private @Nullable PropertyPlaceholderHelper nonStrictHelper; - @Nullable - private PropertyPlaceholderHelper strictHelper; + private @Nullable PropertyPlaceholderHelper strictHelper; private boolean ignoreUnresolvableNestedPlaceholders = false; @@ -58,11 +97,9 @@ public abstract class AbstractPropertyResolver implements ConfigurablePropertyRe private String placeholderSuffix = SystemPropertyUtils.PLACEHOLDER_SUFFIX; - @Nullable - private String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; + private @Nullable String valueSeparator = SystemPropertyUtils.VALUE_SEPARATOR; - @Nullable - private Character escapeCharacter = SystemPropertyUtils.ESCAPE_CHARACTER; + private @Nullable Character escapeCharacter = getDefaultEscapeCharacter(); private final Set requiredProperties = new LinkedHashSet<>(); @@ -91,9 +128,9 @@ public void setConversionService(ConfigurableConversionService conversionService } /** - * 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) { @@ -102,9 +139,9 @@ public void setPlaceholderPrefix(String placeholderPrefix) { } /** - * 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) { @@ -113,11 +150,9 @@ public 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 - * 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) { @@ -125,12 +160,9 @@ public 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. - *

    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) { @@ -175,8 +207,7 @@ public boolean containsProperty(String key) { } @Override - @Nullable - public String getProperty(String key) { + public @Nullable String getProperty(String key) { return getProperty(key, String.class); } @@ -264,8 +295,7 @@ private String doResolvePlaceholders(String text, PropertyPlaceholderHelper help * @since 4.3.5 */ @SuppressWarnings("unchecked") - @Nullable - protected T convertValueIfNecessary(Object value, @Nullable Class targetType) { + protected @Nullable T convertValueIfNecessary(Object value, @Nullable Class targetType) { if (targetType == null) { return (T) value; } @@ -288,7 +318,61 @@ protected T convertValueIfNecessary(Object value, @Nullable Class targetT * @param key the property name to resolve * @return the property value or {@code null} if none found */ - @Nullable - protected abstract String getPropertyAsRawString(String key); + 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/CommandLineArgs.java b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java index d221596667e0..aa3a5f15c855 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLineArgs.java @@ -23,7 +23,7 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A simple representation of command line arguments, broken into @@ -73,8 +73,7 @@ public boolean containsOption(String optionName) { *

    {@code null} signifies that the option was not present on the command * line. An empty list signifies that no values were associated with this option. */ - @Nullable - public List getOptionValues(String optionName) { + public @Nullable List getOptionValues(String optionName) { return this.optionArgs.get(optionName); } diff --git a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java index 20bcc1a95c25..f9b6fbd2ffd0 100644 --- a/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/CommandLinePropertySource.java @@ -19,7 +19,8 @@ import java.util.Collection; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -265,8 +266,7 @@ public final boolean containsProperty(String name) { * {@code null} if there are no such option values. */ @Override - @Nullable - public final String getProperty(String name) { + public final @Nullable String getProperty(String name) { if (this.nonOptionArgsPropertyName.equals(name)) { Collection nonOptionArguments = getNonOptionArgs(); if (nonOptionArguments.isEmpty()) { @@ -306,8 +306,7 @@ public final String getProperty(String name) { *

  8. if the option is not present, return {@code null}
  9. * */ - @Nullable - protected abstract List getOptionValues(String name); + protected abstract @Nullable List getOptionValues(String name); /** * Return the collection of non-option arguments parsed from the command line. 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 5fe8115842c1..646fa3025609 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. @@ -23,7 +23,8 @@ import java.util.List; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -34,7 +35,10 @@ * *

    As of Spring 4.1.2, this class extends {@link EnumerablePropertySource} instead * of plain {@link PropertySource}, exposing {@link #getPropertyNames()} based on the - * accumulated property names from all contained sources (as far as possible). + * accumulated property names from all contained sources - and failing with an + * {@code IllegalStateException} against any non-{@code EnumerablePropertySource}. + * When used through the {@code EnumerablePropertySource} contract, all contained + * sources are expected to be of type {@code EnumerablePropertySource} as well. * * @author Chris Beams * @author Juergen Hoeller @@ -56,8 +60,7 @@ public CompositePropertySource(String name) { @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { for (PropertySource propertySource : this.propertySources) { Object candidate = propertySource.getProperty(name); if (candidate != null) { 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 9de866854fd0..34ecf6623400 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 01f47dae1f62..851f408fb985 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. @@ -16,8 +16,9 @@ package org.springframework.core.env; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.support.ConfigurableConversionService; -import org.springframework.lang.Nullable; /** * Configuration interface to be implemented by most if not all {@link PropertyResolver} @@ -69,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 @@ -106,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 bf83d4e7f941..15bac656983a 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 @@ default boolean matchesProfiles(String... profileExpressions) { * @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/JOptCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java index 679bca5b1438..6a90eed00907 100644 --- a/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/JOptCommandLinePropertySource.java @@ -22,8 +22,8 @@ import joptsimple.OptionSet; import joptsimple.OptionSpec; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -104,8 +104,7 @@ public String[] getPropertyNames() { } @Override - @Nullable - public List getOptionValues(String name) { + public @Nullable List getOptionValues(String name) { List argValues = this.source.valuesOf(name); List stringArgValues = new ArrayList<>(); for (Object argValue : argValues) { diff --git a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java index 36597a5b24a5..975c086b3de2 100644 --- a/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/MapPropertySource.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -45,8 +46,7 @@ public MapPropertySource(String name, Map source) { @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { return this.source.get(name); } diff --git a/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java index bedb7918c15f..4a213dec1b56 100644 --- a/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java +++ b/spring-core/src/main/java/org/springframework/core/env/MutablePropertySources.java @@ -22,7 +22,7 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.stream.Stream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * The default implementation of the {@link PropertySources} interface. @@ -87,8 +87,7 @@ public boolean contains(String name) { } @Override - @Nullable - public PropertySource get(String name) { + public @Nullable PropertySource get(String name) { for (PropertySource propertySource : this.propertySourceList) { if (propertySource.getName().equals(name)) { return propertySource; @@ -155,8 +154,7 @@ public int precedenceOf(PropertySource propertySource) { * Remove and return the property source with the given name, {@code null} if not found. * @param name the name of the property source to find and remove */ - @Nullable - public PropertySource remove(String name) { + public @Nullable PropertySource remove(String name) { synchronized (this.propertySourceList) { int index = this.propertySourceList.indexOf(PropertySource.named(name)); return (index != -1 ? this.propertySourceList.remove(index) : null); diff --git a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java index dcc9474e5dd3..b4398dc6c1be 100644 --- a/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java +++ b/spring-core/src/main/java/org/springframework/core/env/ProfilesParser.java @@ -26,7 +26,8 @@ import java.util.function.Predicate; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** 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 173a1a33784d..dcc034f7523e 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. @@ -16,7 +16,7 @@ package org.springframework.core.env; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface for resolving properties against any underlying source. @@ -30,24 +30,23 @@ 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) * @see #getProperty(String, Class) * @see #getRequiredProperty(String) */ - @Nullable - String getProperty(String key); + @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 @@ -57,17 +56,16 @@ 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 * @see #getRequiredProperty(String, Class) */ - @Nullable - T getProperty(String key, Class targetType); + @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 @@ -77,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/env/PropertySource.java b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java index 3135f0722001..98e91745763b 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySource.java @@ -20,8 +20,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -125,8 +125,7 @@ public boolean containsProperty(String name) { * @param name the property to find * @see PropertyResolver#getRequiredProperty(String) */ - @Nullable - public abstract Object getProperty(String name); + public abstract @Nullable Object getProperty(String name); /** @@ -218,8 +217,7 @@ public StubPropertySource(String name) { * Always returns {@code null}. */ @Override - @Nullable - public String getProperty(String name) { + public @Nullable String getProperty(String name) { return null; } } @@ -251,8 +249,7 @@ public boolean containsProperty(String name) { } @Override - @Nullable - public String getProperty(String name) { + public @Nullable String getProperty(String name) { throw new UnsupportedOperationException(USAGE_ERROR); } } diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySources.java b/spring-core/src/main/java/org/springframework/core/env/PropertySources.java index 0296ea1a271c..d6e6ab80fac3 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertySources.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySources.java @@ -19,7 +19,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Holder containing one or more {@link PropertySource} objects. @@ -49,7 +49,6 @@ default Stream> stream() { * Return the property source with the given name, {@code null} if not found. * @param name the {@linkplain PropertySource#getName() name of the property source} to find */ - @Nullable - PropertySource get(String name); + @Nullable PropertySource get(String name); } diff --git a/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java index 3558d0fa7759..9b35cc2d686c 100644 --- a/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java +++ b/spring-core/src/main/java/org/springframework/core/env/PropertySourcesPropertyResolver.java @@ -16,7 +16,7 @@ package org.springframework.core.env; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link PropertyResolver} implementation that resolves property values against @@ -31,8 +31,7 @@ */ public class PropertySourcesPropertyResolver extends AbstractPropertyResolver { - @Nullable - private final PropertySources propertySources; + private final @Nullable PropertySources propertySources; /** @@ -57,25 +56,21 @@ public boolean containsProperty(String key) { } @Override - @Nullable - public String getProperty(String key) { + public @Nullable String getProperty(String key) { return getProperty(key, String.class, true); } @Override - @Nullable - public T getProperty(String key, Class targetValueType) { + public @Nullable T getProperty(String key, Class targetValueType) { return getProperty(key, targetValueType, true); } @Override - @Nullable - protected String getPropertyAsRawString(String key) { + protected @Nullable String getPropertyAsRawString(String key) { return getProperty(key, String.class, false); } - @Nullable - protected T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { + protected @Nullable T getProperty(String key, Class targetValueType, boolean resolveNestedPlaceholders) { if (this.propertySources != null) { for (PropertySource propertySource : this.propertySources) { if (logger.isTraceEnabled()) { diff --git a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java index 1c4591f3dff5..05d944cff317 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/SimpleCommandLinePropertySource.java @@ -18,7 +18,8 @@ import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -129,8 +130,7 @@ protected boolean containsOption(String name) { } @Override - @Nullable - protected List getOptionValues(String name) { + protected @Nullable List getOptionValues(String name) { return this.source.getOptionValues(name); } diff --git a/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java index ebea27125309..33e088597d72 100644 --- a/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/env/SystemEnvironmentPropertySource.java @@ -19,7 +19,8 @@ import java.util.Locale; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -89,8 +90,7 @@ public boolean containsProperty(String name) { * any underscore/uppercase variant thereof exists in this property source. */ @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { String actualName = resolvePropertyName(name); if (logger.isDebugEnabled() && !name.equals(actualName)) { logger.debug("PropertySource '" + getName() + "' does not contain property '" + name + @@ -120,8 +120,7 @@ protected final String resolvePropertyName(String name) { return name; } - @Nullable - private String checkPropertyName(String name) { + private @Nullable String checkPropertyName(String name) { // Check name as-is if (this.source.containsKey(name)) { return name; diff --git a/spring-core/src/main/java/org/springframework/core/env/package-info.java b/spring-core/src/main/java/org/springframework/core/env/package-info.java index a0784c4c8d76..d2253a9b0496 100644 --- a/spring-core/src/main/java/org/springframework/core/env/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/env/package-info.java @@ -2,9 +2,7 @@ * Spring's environment abstraction consisting of bean definition * profile and hierarchical property source support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.env; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 84eff6587bea..b65ff3faacfe 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.file.NoSuchFileException; import java.nio.file.StandardOpenOption; import java.util.jar.JarEntry; +import java.util.jar.JarFile; import org.springframework.util.ResourceUtils; @@ -56,6 +57,7 @@ public boolean exists() { // Try a URL connection content-length header URLConnection con = url.openConnection(); customizeConnection(con); + HttpURLConnection httpCon = (con instanceof HttpURLConnection huc ? huc : null); if (httpCon != null) { httpCon.setRequestMethod("HEAD"); @@ -81,12 +83,26 @@ else if (code == HttpURLConnection.HTTP_NOT_FOUND) { } } } - // Check content-length entry but not for JarURLConnection where - // this would open the jar file but effectively never close it -> - // for jar entries, always fall back to stream existence instead. - if (!(con instanceof JarURLConnection) && con.getContentLengthLong() > 0) { + + 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). + // 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; } + if (httpCon != null) { // No HTTP OK status, and no content-length header: give up httpCon.disconnect(); @@ -346,8 +362,8 @@ public long lastModified() throws IOException { */ protected void customizeConnection(URLConnection con) throws IOException { ResourceUtils.useCachesIfNecessary(con); - if (con instanceof HttpURLConnection httpConn) { - customizeConnection(httpConn); + if (con instanceof HttpURLConnection httpCon) { + customizeConnection(httpCon); } } diff --git a/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java b/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java index 4e70092fb793..87b98ed8ce90 100644 --- a/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/AbstractResource.java @@ -29,8 +29,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ResourceUtils; /** @@ -215,8 +215,7 @@ public Resource createRelative(String relativePath) throws IOException { * assuming that this resource type does not have a filename. */ @Override - @Nullable - public String getFilename() { + public @Nullable String getFilename() { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java b/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java index b0f84a946030..03af0cbc92a4 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/ByteArrayResource.java @@ -22,7 +22,8 @@ import java.nio.charset.Charset; import java.util.Arrays; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java index 1454b92f8057..72e8c532ddf0 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/ClassPathResource.java @@ -21,7 +21,8 @@ import java.io.InputStream; import java.net.URL; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -53,11 +54,9 @@ public class ClassPathResource extends AbstractFileResolvingResource { private final String absolutePath; - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; - @Nullable - private final Class clazz; + private final @Nullable Class clazz; /** @@ -140,8 +139,7 @@ public final String getPath() { /** * Return the {@link ClassLoader} that this resource will be obtained from. */ - @Nullable - public final ClassLoader getClassLoader() { + public final @Nullable ClassLoader getClassLoader() { return (this.clazz != null ? this.clazz.getClassLoader() : this.classLoader); } @@ -172,8 +170,7 @@ public boolean isReadable() { * Resolves a {@link URL} for the underlying class path resource. * @return the resolved URL, or {@code null} if not resolvable */ - @Nullable - protected URL resolveURL() { + protected @Nullable URL resolveURL() { try { if (this.clazz != null) { return this.clazz.getResource(this.path); @@ -250,8 +247,7 @@ public Resource createRelative(String relativePath) { * @see StringUtils#getFilename(String) */ @Override - @Nullable - public String getFilename() { + public @Nullable String getFilename() { return StringUtils.getFilename(this.absolutePath); } 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 c7a7403a748e..bc2594123da1 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. @@ -24,7 +24,8 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ResourceUtils; @@ -48,8 +49,7 @@ */ public class DefaultResourceLoader implements ResourceLoader { - @Nullable - private ClassLoader classLoader; + private @Nullable ClassLoader classLoader; private final Set protocolResolvers = new LinkedHashSet<>(4); @@ -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 DefaultResourceLoader(@Nullable ClassLoader classLoader) { * 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; @@ -93,8 +93,7 @@ public void setClassLoader(@Nullable ClassLoader classLoader) { * @see ClassPathResource */ @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return (this.classLoader != null ? this.classLoader : ClassUtils.getDefaultClassLoader()); } diff --git a/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java b/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java index 49d9d854a965..1c600f5bb013 100644 --- a/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/DescriptiveResource.java @@ -20,7 +20,7 @@ import java.io.IOException; import java.io.InputStream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple {@link Resource} implementation that holds a resource description diff --git a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java index fcae1575c6b9..acd8c98abdb9 100644 --- a/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/FileSystemResource.java @@ -34,7 +34,8 @@ import java.nio.file.Path; import java.nio.file.StandardOpenOption; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -63,8 +64,7 @@ public class FileSystemResource extends AbstractResource implements WritableReso private final String path; - @Nullable - private final File file; + private final @Nullable File file; private final Path filePath; diff --git a/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java b/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java index b910bbc9165b..7bb38ad93d68 100644 --- a/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/FileUrlResource.java @@ -26,7 +26,8 @@ import java.nio.file.Files; import java.nio.file.StandardOpenOption; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ResourceUtils; /** @@ -45,8 +46,7 @@ */ public class FileUrlResource extends UrlResource implements WritableResource { - @Nullable - private volatile File file; + private volatile @Nullable File file; /** diff --git a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java index 906eb233a6c6..1666d1e9399a 100644 --- a/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/InputStreamResource.java @@ -19,7 +19,8 @@ import java.io.IOException; import java.io.InputStream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java b/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java index 225a6042a397..0413f83667f8 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/ModuleResource.java @@ -20,7 +20,8 @@ import java.io.IOException; import java.io.InputStream; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -93,8 +94,7 @@ public Resource createRelative(String relativePath) { } @Override - @Nullable - public String getFilename() { + public @Nullable String getFilename() { return StringUtils.getFilename(this.path); } diff --git a/spring-core/src/main/java/org/springframework/core/io/PathResource.java b/spring-core/src/main/java/org/springframework/core/io/PathResource.java index 729f08023015..e5f39a7f866a 100644 --- a/spring-core/src/main/java/org/springframework/core/io/PathResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/PathResource.java @@ -33,7 +33,8 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -55,7 +56,9 @@ * @see java.nio.file.Path * @see java.nio.file.Files * @see FileSystemResource + * @deprecated since 7.0 in favor of {@link FileSystemResource} */ +@Deprecated(since = "7.0", forRemoval = true) public class PathResource extends AbstractResource implements WritableResource { private final Path path; diff --git a/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java b/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java index 53cc9301eb60..cf0def7ca8c9 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java +++ b/spring-core/src/main/java/org/springframework/core/io/ProtocolResolver.java @@ -16,7 +16,7 @@ package org.springframework.core.io; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A resolution strategy for protocol-specific resource handles. @@ -40,7 +40,6 @@ public interface ProtocolResolver { * @return a corresponding {@code Resource} handle if the given location * matches this resolver's protocol, or {@code null} otherwise */ - @Nullable - Resource resolve(String location, ResourceLoader resourceLoader); + @Nullable Resource resolve(String location, ResourceLoader resourceLoader); } diff --git a/spring-core/src/main/java/org/springframework/core/io/Resource.java b/spring-core/src/main/java/org/springframework/core/io/Resource.java index 91458934fed3..44975e1fdccd 100644 --- a/spring-core/src/main/java/org/springframework/core/io/Resource.java +++ b/spring-core/src/main/java/org/springframework/core/io/Resource.java @@ -26,7 +26,8 @@ import java.nio.channels.ReadableByteChannel; import java.nio.charset.Charset; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.FileCopyUtils; /** @@ -193,8 +194,7 @@ default String getContentAsString(Charset charset) throws IOException { * have a filename. *

    Implementations are encouraged to return the filename unencoded. */ - @Nullable - String getFilename(); + @Nullable String getFilename(); /** * Return a description for this resource, diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java b/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java index 0eec6fd6a3d8..1cb67aa89880 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java +++ b/spring-core/src/main/java/org/springframework/core/io/ResourceEditor.java @@ -19,9 +19,10 @@ import java.beans.PropertyEditorSupport; import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.StandardEnvironment; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -51,8 +52,7 @@ public class ResourceEditor extends PropertyEditorSupport { private final ResourceLoader resourceLoader; - @Nullable - private PropertyResolver propertyResolver; + private @Nullable PropertyResolver propertyResolver; private final boolean ignoreUnresolvablePlaceholders; @@ -122,8 +122,7 @@ protected String resolvePath(String path) { @Override - @Nullable - public String getAsText() { + public @Nullable String getAsText() { Resource value = (Resource) getValue(); try { // Try to determine URL for resource. diff --git a/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java b/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java index 2c58f931d187..cc44f95cfc8d 100644 --- a/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java +++ b/spring-core/src/main/java/org/springframework/core/io/ResourceLoader.java @@ -16,7 +16,8 @@ package org.springframework.core.io; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ResourceUtils; /** @@ -76,7 +77,6 @@ public interface ResourceLoader { * @see org.springframework.util.ClassUtils#getDefaultClassLoader() * @see org.springframework.util.ClassUtils#forName(String, ClassLoader) */ - @Nullable - ClassLoader getClassLoader(); + @Nullable ClassLoader getClassLoader(); } diff --git a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java b/spring-core/src/main/java/org/springframework/core/io/UrlResource.java index 4c5c3e0226d3..aa5087f8d9f5 100644 --- a/spring-core/src/main/java/org/springframework/core/io/UrlResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/UrlResource.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,7 +30,8 @@ import java.nio.charset.StandardCharsets; import java.util.Base64; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -53,8 +54,7 @@ public class UrlResource extends AbstractFileResolvingResource { /** * Original URI, if available; used for URI and File access. */ - @Nullable - private final URI uri; + private final @Nullable URI uri; /** * Original URL, used for actual access. @@ -64,8 +64,7 @@ public class UrlResource extends AbstractFileResolvingResource { /** * Cleaned URL String (with normalized path), used for comparisons. */ - @Nullable - private volatile String cleanedUrl; + private volatile @Nullable String cleanedUrl; /** @@ -234,8 +233,8 @@ public InputStream getInputStream() throws IOException { } catch (IOException ex) { // Close the HTTP connection (if applicable). - if (con instanceof HttpURLConnection httpConn) { - httpConn.disconnect(); + if (con instanceof HttpURLConnection httpCon) { + httpCon.disconnect(); } throw ex; } @@ -331,8 +330,7 @@ protected URL createRelativeURL(String relativePath) throws MalformedURLExceptio * @see java.net.URLDecoder#decode(String, java.nio.charset.Charset) */ @Override - @Nullable - public String getFilename() { + public @Nullable String getFilename() { if (this.uri != null) { String path = this.uri.getPath(); if (path != null) { diff --git a/spring-core/src/main/java/org/springframework/core/io/VfsResource.java b/spring-core/src/main/java/org/springframework/core/io/VfsResource.java index 00203ed3b923..16f2a7757ead 100644 --- a/spring-core/src/main/java/org/springframework/core/io/VfsResource.java +++ b/spring-core/src/main/java/org/springframework/core/io/VfsResource.java @@ -22,7 +22,8 @@ import java.net.URI; import java.net.URL; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; diff --git a/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java b/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java index 781712a224aa..6ecdbf2c3b97 100644 --- a/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/VfsUtils.java @@ -25,7 +25,8 @@ import java.net.URI; import java.net.URL; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ReflectionUtils; /** @@ -184,13 +185,11 @@ protected static Object getRoot(URL url) throws IOException { return invokeVfsMethod(VFS_METHOD_GET_ROOT_URL, null, url); } - @Nullable - protected static Object doGetVisitorAttributes() { + protected static @Nullable Object doGetVisitorAttributes() { return ReflectionUtils.getField(VISITOR_ATTRIBUTES_FIELD_RECURSE, null); } - @Nullable - protected static String doGetPath(Object resource) { + protected static @Nullable String doGetPath(Object resource) { return (String) ReflectionUtils.invokeMethod(VIRTUAL_FILE_METHOD_GET_PATH_NAME, resource); } 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 33135285ae95..460cf8df2c59 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 @@ 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 @@ public void close() { 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(Math.min(this.end, 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/DataBufferUtils.java b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java index e8de69083d99..87aef781cce4 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/DataBufferUtils.java @@ -41,6 +41,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -52,7 +53,6 @@ import reactor.util.context.Context; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; 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 1de857f6293e..cfba5927323c 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 int capacity() { } @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 DataBuffer write(CharSequence charSequence, } @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 DataBuffer split(int index) { } @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 d9d43da4eeaf..ece053d4b7fd 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 @@ -22,7 +22,8 @@ import java.util.NoSuchElementException; import java.util.function.IntPredicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -179,7 +180,7 @@ public int capacity() { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { setCapacity(capacity); return this; @@ -339,7 +340,7 @@ private void write(ByteBuffer source) { } @Override - @Deprecated + @Deprecated(since = "6.0") public DefaultDataBuffer slice(int index, int length) { checkIndex(index, length); int oldPosition = this.byteBuffer.position(); @@ -379,13 +380,13 @@ public DefaultDataBuffer split(int index) { } @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); @@ -396,7 +397,7 @@ public ByteBuffer asByteBuffer(int index, int length) { } @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 81ed6242bdf7..5572f29c07a7 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 DefaultDataBufferFactory(boolean preferDirect, int defaultInitialCapacity @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 5d211bfc314a..5d1c73aa954c 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 @@ -22,8 +22,8 @@ import java.util.function.IntPredicate; import org.eclipse.jetty.io.Content; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,8 +39,7 @@ public final class JettyDataBuffer implements PooledDataBuffer { private final DefaultDataBuffer delegate; - @Nullable - private final Content.Chunk chunk; + private final Content.@Nullable Chunk chunk; private final JettyDataBufferFactory bufferFactory; @@ -139,7 +138,7 @@ public int capacity() { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer capacity(int capacity) { this.delegate.capacity(capacity); return this; @@ -226,7 +225,7 @@ public DataBuffer write(ByteBuffer... buffers) { } @Override - @Deprecated + @Deprecated(since = "6.0") public DataBuffer slice(int index, int length) { DefaultDataBuffer delegateSlice = this.delegate.slice(index, length); if (this.chunk != null) { @@ -251,19 +250,19 @@ public DataBuffer split(int index) { } @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 02a78c02745f..c22fed6a1bad 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 JettyDataBufferFactory(boolean preferDirect, int defaultInitialCapacity) @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/Netty5DataBuffer.java b/spring-core/src/main/java/org/springframework/core/io/buffer/Netty5DataBuffer.java deleted file mode 100644 index 70a544cd4f0f..000000000000 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/Netty5DataBuffer.java +++ /dev/null @@ -1,402 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.io.buffer; - -import java.nio.ByteBuffer; -import java.nio.charset.Charset; -import java.util.NoSuchElementException; -import java.util.function.IntPredicate; - -import io.netty5.buffer.Buffer; -import io.netty5.buffer.BufferComponent; -import io.netty5.buffer.ComponentIterator; - -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; - -/** - * Implementation of the {@code DataBuffer} interface that wraps a Netty 5 - * {@link Buffer}. Typically constructed with {@link Netty5DataBufferFactory}. - * - * @author Violeta Georgieva - * @author Arjen Poutsma - * @since 6.0 - */ -public final class Netty5DataBuffer implements CloseableDataBuffer, TouchableDataBuffer { - - private final Buffer buffer; - - private final Netty5DataBufferFactory dataBufferFactory; - - - /** - * Create a new {@code Netty5DataBuffer} based on the given {@code Buffer}. - * @param buffer the buffer to base this buffer on - */ - Netty5DataBuffer(Buffer buffer, Netty5DataBufferFactory dataBufferFactory) { - Assert.notNull(buffer, "Buffer must not be null"); - Assert.notNull(dataBufferFactory, "Netty5DataBufferFactory must not be null"); - this.buffer = buffer; - this.dataBufferFactory = dataBufferFactory; - } - - /** - * Directly exposes the native {@code Buffer} that this buffer is based on. - * @return the wrapped buffer - */ - public Buffer getNativeBuffer() { - return this.buffer; - } - - @Override - public DataBufferFactory factory() { - return this.dataBufferFactory; - } - - @Override - public int indexOf(IntPredicate predicate, int fromIndex) { - Assert.notNull(predicate, "IntPredicate must not be null"); - if (fromIndex < 0) { - fromIndex = 0; - } - else if (fromIndex >= this.buffer.writerOffset()) { - return -1; - } - int length = this.buffer.writerOffset() - fromIndex; - int bytes = this.buffer.openCursor(fromIndex, length).process(predicate.negate()::test); - return bytes == -1 ? -1 : fromIndex + bytes; - } - - @Override - public int lastIndexOf(IntPredicate predicate, int fromIndex) { - Assert.notNull(predicate, "IntPredicate must not be null"); - if (fromIndex < 0) { - return -1; - } - fromIndex = Math.min(fromIndex, this.buffer.writerOffset() - 1); - return this.buffer.openCursor(0, fromIndex + 1).process(predicate.negate()::test); - } - - @Override - public int readableByteCount() { - return this.buffer.readableBytes(); - } - - @Override - public int writableByteCount() { - return this.buffer.writableBytes(); - } - - @Override - public int readPosition() { - return this.buffer.readerOffset(); - } - - @Override - public Netty5DataBuffer readPosition(int readPosition) { - this.buffer.readerOffset(readPosition); - return this; - } - - @Override - public int writePosition() { - return this.buffer.writerOffset(); - } - - @Override - public Netty5DataBuffer writePosition(int writePosition) { - this.buffer.writerOffset(writePosition); - return this; - } - - @Override - public byte getByte(int index) { - return this.buffer.getByte(index); - } - - @Override - public int capacity() { - return this.buffer.capacity(); - } - - @Override - @Deprecated - public Netty5DataBuffer capacity(int capacity) { - if (capacity <= 0) { - throw new IllegalArgumentException(String.format("'newCapacity' %d must be higher than 0", capacity)); - } - int diff = capacity - capacity(); - if (diff > 0) { - this.buffer.ensureWritable(this.buffer.writableBytes() + diff); - } - return this; - } - - @Override - public DataBuffer ensureWritable(int capacity) { - Assert.isTrue(capacity >= 0, "Capacity must be >= 0"); - this.buffer.ensureWritable(capacity); - return this; - } - - @Override - public byte read() { - return this.buffer.readByte(); - } - - @Override - public Netty5DataBuffer read(byte[] destination) { - return read(destination, 0, destination.length); - } - - @Override - public Netty5DataBuffer read(byte[] destination, int offset, int length) { - this.buffer.readBytes(destination, offset, length); - return this; - } - - @Override - public Netty5DataBuffer write(byte b) { - this.buffer.writeByte(b); - return this; - } - - @Override - public Netty5DataBuffer write(byte[] source) { - this.buffer.writeBytes(source); - return this; - } - - @Override - public Netty5DataBuffer write(byte[] source, int offset, int length) { - this.buffer.writeBytes(source, offset, length); - return this; - } - - @Override - public Netty5DataBuffer write(DataBuffer... dataBuffers) { - if (!ObjectUtils.isEmpty(dataBuffers)) { - if (hasNetty5DataBuffers(dataBuffers)) { - Buffer[] nativeBuffers = new Buffer[dataBuffers.length]; - for (int i = 0; i < dataBuffers.length; i++) { - nativeBuffers[i] = ((Netty5DataBuffer) dataBuffers[i]).getNativeBuffer(); - } - return write(nativeBuffers); - } - else { - ByteBuffer[] byteBuffers = new ByteBuffer[dataBuffers.length]; - for (int i = 0; i < dataBuffers.length; i++) { - byteBuffers[i] = ByteBuffer.allocate(dataBuffers[i].readableByteCount()); - dataBuffers[i].toByteBuffer(byteBuffers[i]); - } - return write(byteBuffers); - } - } - return this; - } - - private static boolean hasNetty5DataBuffers(DataBuffer[] buffers) { - for (DataBuffer buffer : buffers) { - if (!(buffer instanceof Netty5DataBuffer)) { - return false; - } - } - return true; - } - - @Override - public Netty5DataBuffer write(ByteBuffer... buffers) { - if (!ObjectUtils.isEmpty(buffers)) { - for (ByteBuffer buffer : buffers) { - this.buffer.writeBytes(buffer); - } - } - return this; - } - - /** - * Writes one or more Netty 5 {@link Buffer Buffers} to this buffer, - * starting at the current writing position. - * @param buffers the buffers to write into this buffer - * @return this buffer - */ - public Netty5DataBuffer write(Buffer... buffers) { - if (!ObjectUtils.isEmpty(buffers)) { - for (Buffer buffer : buffers) { - this.buffer.writeBytes(buffer); - } - } - return this; - } - - @Override - public DataBuffer write(CharSequence charSequence, Charset charset) { - Assert.notNull(charSequence, "CharSequence must not be null"); - Assert.notNull(charset, "Charset must not be null"); - - this.buffer.writeCharSequence(charSequence, charset); - return this; - } - - /** - * {@inheritDoc} - *

    Note that due to the lack of a {@code slice} method - * in Netty 5's {@link Buffer}, this implementation returns a copy that - * does not share its contents with this buffer. - */ - @Override - @Deprecated - public DataBuffer slice(int index, int length) { - Buffer copy = this.buffer.copy(index, length); - return new Netty5DataBuffer(copy, this.dataBufferFactory); - } - - @Override - public DataBuffer split(int index) { - Buffer split = this.buffer.split(index); - return new Netty5DataBuffer(split, this.dataBufferFactory); - } - - @Override - @Deprecated - public ByteBuffer asByteBuffer() { - return toByteBuffer(); - } - - @Override - @Deprecated - public ByteBuffer asByteBuffer(int index, int length) { - return toByteBuffer(index, length); - } - - @Override - @Deprecated - public ByteBuffer toByteBuffer(int index, int length) { - ByteBuffer copy = this.buffer.isDirect() ? - ByteBuffer.allocateDirect(length) : - ByteBuffer.allocate(length); - - this.buffer.copyInto(index, copy, 0, length); - return copy; - } - - @Override - public void toByteBuffer(int srcPos, ByteBuffer dest, int destPos, int length) { - this.buffer.copyInto(srcPos, dest, destPos, length); - } - - @Override - public ByteBufferIterator readableByteBuffers() { - return new BufferComponentIterator<>(this.buffer.forEachComponent(), true); - } - - @Override - public ByteBufferIterator writableByteBuffers() { - return new BufferComponentIterator<>(this.buffer.forEachComponent(), false); - } - - @Override - public String toString(Charset charset) { - Assert.notNull(charset, "Charset must not be null"); - return this.buffer.toString(charset); - } - - @Override - public String toString(int index, int length, Charset charset) { - Assert.notNull(charset, "Charset must not be null"); - byte[] data = new byte[length]; - this.buffer.copyInto(index, data, 0, length); - return new String(data, 0, length, charset); - } - - @Override - public Netty5DataBuffer touch(Object hint) { - this.buffer.touch(hint); - return this; - } - - @Override - public void close() { - this.buffer.close(); - } - - - @Override - public boolean equals(@Nullable Object other) { - return (this == other || (other instanceof Netty5DataBuffer that && this.buffer.equals(that.buffer))); - } - - @Override - public int hashCode() { - return this.buffer.hashCode(); - } - - @Override - public String toString() { - return this.buffer.toString(); - } - - - private static final class BufferComponentIterator - implements ByteBufferIterator { - - private final ComponentIterator delegate; - - private final boolean readable; - - @Nullable - private T next; - - public BufferComponentIterator(ComponentIterator delegate, boolean readable) { - Assert.notNull(delegate, "Delegate must not be null"); - this.delegate = delegate; - this.readable = readable; - this.next = readable ? this.delegate.firstReadable() : this.delegate.firstWritable(); - } - - @Override - public boolean hasNext() { - return this.next != null; - } - - @Override - public ByteBuffer next() { - if (this.next != null) { - ByteBuffer result; - if (this.readable) { - result = this.next.readableBuffer(); - this.next = this.next.nextReadable(); - } - else { - result = this.next.writableBuffer(); - this.next = this.next.nextWritable(); - } - return result; - } - else { - throw new NoSuchElementException(); - } - } - - @Override - public void close() { - this.delegate.close(); - } - } - -} diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/Netty5DataBufferFactory.java b/spring-core/src/main/java/org/springframework/core/io/buffer/Netty5DataBufferFactory.java deleted file mode 100644 index 7163274b61ca..000000000000 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/Netty5DataBufferFactory.java +++ /dev/null @@ -1,141 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.io.buffer; - -import java.nio.ByteBuffer; -import java.util.List; - -import io.netty5.buffer.Buffer; -import io.netty5.buffer.BufferAllocator; -import io.netty5.buffer.CompositeBuffer; -import io.netty5.buffer.DefaultBufferAllocators; - -import org.springframework.util.Assert; - -/** - * Implementation of the {@code DataBufferFactory} interface based on a - * Netty 5 {@link BufferAllocator}. - * - * @author Violeta Georgieva - * @author Arjen Poutsma - * @since 6.0 - */ -public class Netty5DataBufferFactory implements DataBufferFactory { - - private final BufferAllocator bufferAllocator; - - - /** - * Create a new {@code Netty5DataBufferFactory} based on the given factory. - * @param bufferAllocator the factory to use - */ - public Netty5DataBufferFactory(BufferAllocator bufferAllocator) { - Assert.notNull(bufferAllocator, "BufferAllocator must not be null"); - this.bufferAllocator = bufferAllocator; - } - - - /** - * Return the {@code BufferAllocator} used by this factory. - */ - public BufferAllocator getBufferAllocator() { - return this.bufferAllocator; - } - - @Override - @Deprecated - public Netty5DataBuffer allocateBuffer() { - Buffer buffer = this.bufferAllocator.allocate(256); - return new Netty5DataBuffer(buffer, this); - } - - @Override - public Netty5DataBuffer allocateBuffer(int initialCapacity) { - Buffer buffer = this.bufferAllocator.allocate(initialCapacity); - return new Netty5DataBuffer(buffer, this); - } - - @Override - public Netty5DataBuffer wrap(ByteBuffer byteBuffer) { - Buffer buffer = this.bufferAllocator.copyOf(byteBuffer); - return new Netty5DataBuffer(buffer, this); - } - - @Override - public Netty5DataBuffer wrap(byte[] bytes) { - Buffer buffer = this.bufferAllocator.copyOf(bytes); - return new Netty5DataBuffer(buffer, this); - } - - /** - * Wrap the given Netty {@link Buffer} in a {@code Netty5DataBuffer}. - * @param buffer the Netty buffer to wrap - * @return the wrapped buffer - */ - public Netty5DataBuffer wrap(Buffer buffer) { - buffer.touch("Wrap buffer"); - return new Netty5DataBuffer(buffer, this); - } - - /** - * {@inheritDoc} - *

    This implementation uses Netty's {@link CompositeBuffer}. - */ - @Override - public DataBuffer join(List dataBuffers) { - Assert.notEmpty(dataBuffers, "DataBuffer List must not be empty"); - if (dataBuffers.size() == 1) { - return dataBuffers.get(0); - } - CompositeBuffer composite = this.bufferAllocator.compose(); - for (DataBuffer dataBuffer : dataBuffers) { - Assert.isInstanceOf(Netty5DataBuffer.class, dataBuffer); - composite.extendWith(((Netty5DataBuffer) dataBuffer).getNativeBuffer().send()); - } - return new Netty5DataBuffer(composite, this); - } - - @Override - public boolean isDirect() { - return this.bufferAllocator.getAllocationType().isDirect(); - } - - /** - * Return the given Netty {@link DataBuffer} as a {@link Buffer}. - *

    Returns the {@linkplain Netty5DataBuffer#getNativeBuffer() native buffer} - * if {@code buffer} is a {@link Netty5DataBuffer}; returns - * {@link BufferAllocator#copyOf(ByteBuffer)} otherwise. - * @param buffer the {@code DataBuffer} to return a {@code Buffer} for - * @return the netty {@code Buffer} - */ - public static Buffer toBuffer(DataBuffer buffer) { - if (buffer instanceof Netty5DataBuffer netty5DataBuffer) { - return netty5DataBuffer.getNativeBuffer(); - } - else { - ByteBuffer byteBuffer = ByteBuffer.allocate(buffer.readableByteCount()); - buffer.toByteBuffer(byteBuffer); - return DefaultBufferAllocators.preferredAllocator().copyOf(byteBuffer); - } - } - - - @Override - public String toString() { - return "Netty5DataBufferFactory (" + this.bufferAllocator + ")"; - } -} 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 ddcc6c6f9b39..ec695cbb79f2 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 @@ -24,8 +24,8 @@ import io.netty.buffer.ByteBuf; import io.netty.buffer.ByteBufUtil; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -135,7 +135,7 @@ public int capacity() { } @Override - @Deprecated + @Deprecated(since = "6.0") public NettyDataBuffer capacity(int capacity) { this.byteBuf.capacity(capacity); return this; @@ -255,14 +255,14 @@ else if (StandardCharsets.US_ASCII.equals(charset)) { } @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 NettyDataBuffer split(int index) { } @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 40082b437123..2f1ff84ec0e3 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 ByteBufAllocator getByteBufAllocator() { } @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/buffer/OutputStreamPublisher.java b/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java index dc6cad91014d..4dadbd61633e 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/OutputStreamPublisher.java @@ -25,11 +25,11 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.locks.LockSupport; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -139,8 +139,7 @@ private static final class OutputStreamSubscription extends OutputStream impl private final AtomicReference parkedThread = new AtomicReference<>(); - @Nullable - private volatile Throwable error; + private volatile @Nullable Throwable error; private long produced; diff --git a/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java b/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java index 4fc6d0eae76d..505260af86d1 100644 --- a/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java +++ b/spring-core/src/main/java/org/springframework/core/io/buffer/SubscriberInputStream.java @@ -28,11 +28,11 @@ import java.util.concurrent.locks.LockSupport; import java.util.concurrent.locks.ReentrantLock; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; import reactor.core.Exceptions; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -80,16 +80,13 @@ final class SubscriberInputStream extends InputStream implements Subscriber isNotSystemModule = resolvedModule -> !systemModuleNames.contains(resolvedModule.name()); - @Nullable - private static Method equinoxResolveMethod; + private static @Nullable Method equinoxResolveMethod; static { try { @@ -257,12 +259,13 @@ 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<>(); - @Nullable - private volatile Set manifestEntriesCache; + private volatile @Nullable Set manifestEntriesCache; /** @@ -308,8 +311,7 @@ public ResourceLoader getResourceLoader() { } @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return getResourceLoader().getClassLoader(); } @@ -331,6 +333,22 @@ public PathMatcher getPathMatcher() { return this.pathMatcher; } + /** + * Specify whether this resolver should use jar caches. Default is {@code true}. + *

    Switch this flag to {@code false} in order to avoid 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) { @@ -354,7 +372,7 @@ public Resource[] getResources(String locationPattern) throws IOException { // all class path resources with the given name Collections.addAll(resources, findAllClassPathResources(locationPatternWithoutPrefix)); } - return resources.toArray(new Resource[0]); + return resources.toArray(EMPTY_RESOURCE_ARRAY); } else { // Generally only look for a pattern after a prefix here, @@ -398,7 +416,7 @@ protected Resource[] findAllClassPathResources(String location) throws IOExcepti if (logger.isTraceEnabled()) { logger.trace("Resolved class path location [" + path + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -535,7 +553,9 @@ protected void addClassPathManifestEntries(Set result) { 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()) && @@ -687,7 +707,9 @@ else if (commonPrefix.equals(rootDirPath)) { 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); + } } } @@ -719,7 +741,7 @@ else if (ResourceUtils.isJarURL(rootDirUrl) || isJarResource(rootDirResource)) { if (logger.isTraceEnabled()) { logger.trace("Resolved location pattern [" + locationPattern + "] to resources " + result); } - return result.toArray(new Resource[0]); + return result.toArray(EMPTY_RESOURCE_ARRAY); } /** @@ -840,6 +862,9 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, if (con instanceof JarURLConnection jarCon) { // Should usually be the case for traditional JAR files. + if (!this.useCaches) { + jarCon.setUseCaches(false); + } try { jarFile = jarCon.getJarFile(); jarFileUrl = jarCon.getJarFileURL().toExternalForm(); @@ -847,9 +872,9 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, 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(); } } @@ -903,8 +928,10 @@ protected Set doFindPathMatchingJarResources(Resource rootDirResource, } } } - // 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 { @@ -1097,8 +1124,7 @@ protected Set findAllModulePathResources(String locationPattern) throw return result; } - @Nullable - private Resource findResource(ModuleReader moduleReader, String name) { + private @Nullable Resource findResource(ModuleReader moduleReader, String name) { try { return moduleReader.find(name) .map(this::convertModuleSystemURI) @@ -1168,8 +1194,7 @@ public PatternVirtualFileVisitor(String rootPath, String subPattern, PathMatcher } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (Object.class == method.getDeclaringClass()) { switch (methodName) { @@ -1200,8 +1225,7 @@ public void visit(Object vfsResource) { } } - @Nullable - public Object getAttributes() { + public @Nullable Object getAttributes() { return VfsPatternUtils.getVisitorAttributes(); } @@ -1246,12 +1270,11 @@ private static String fixPath(String path) { } /** - * 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} */ - @Nullable - private static Resource createAlternative(String path) { + private static @Nullable Resource createAlternative(String path) { try { String alternativePath = path.startsWith("/") ? path.substring(1) : "/" + path; return asJarFileResource(alternativePath); 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 85d53d40475a..9c04d198b36f 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. @@ -24,9 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.DefaultPropertiesPersister; import org.springframework.util.PropertiesPersister; @@ -44,18 +44,15 @@ public abstract class PropertiesLoaderSupport { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - protected Properties[] localProperties; + protected Properties @Nullable [] localProperties; protected boolean localOverride = false; - @Nullable - private Resource[] locations; + private Resource @Nullable [] locations; private boolean ignoreResourceNotFound = false; - @Nullable - private String fileEncoding; + private @Nullable String fileEncoding; private PropertiesPersister propertiesPersister = DefaultPropertiesPersister.INSTANCE; @@ -141,8 +138,8 @@ public void setPropertiesPersister(@Nullable PropertiesPersister propertiesPersi /** - * 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/PropertiesLoaderUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java index af5d58e8ec9e..241f1ee552ad 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertiesLoaderUtils.java @@ -24,8 +24,9 @@ import java.util.Enumeration; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.DefaultPropertiesPersister; diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java index 6c3059b52787..7b55af24dce8 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceDescriptor.java @@ -19,7 +19,7 @@ import java.util.Arrays; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Descriptor for a {@link org.springframework.core.env.PropertySource PropertySource}. diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java index 3de69fca7c39..3ee58ad9eee2 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceFactory.java @@ -18,8 +18,9 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.PropertySource; -import org.springframework.lang.Nullable; /** * Strategy interface for creating resource-based {@link PropertySource} wrappers. diff --git a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java index 02f0370b2256..33174276ec3a 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/PropertySourceProcessor.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.CompositePropertySource; import org.springframework.core.env.ConfigurableEnvironment; @@ -34,7 +35,6 @@ import org.springframework.core.env.PropertySource; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.PlaceholderResolutionException; import org.springframework.util.ReflectionUtils; diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java index 0a16606c9cc1..cb30e846c17f 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourceArrayPropertyEditor.java @@ -26,12 +26,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.env.Environment; import org.springframework.core.env.PropertyResolver; import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -64,8 +64,7 @@ public class ResourceArrayPropertyEditor extends PropertyEditorSupport { private final ResourcePatternResolver resourcePatternResolver; - @Nullable - private PropertyResolver propertyResolver; + private @Nullable PropertyResolver propertyResolver; private final boolean ignoreUnresolvablePlaceholders; diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java index c6c778430b61..a3e4e3b01366 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePatternUtils.java @@ -16,8 +16,9 @@ package org.springframework.core.io.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.ResourceUtils; /** diff --git a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java index ad9e57aa4b2b..342588ec7b49 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/ResourcePropertySource.java @@ -20,10 +20,11 @@ import java.util.Map; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.PropertiesPropertySource; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -45,8 +46,7 @@ public class ResourcePropertySource extends PropertiesPropertySource { /** The original resource name, if different from the given name. */ - @Nullable - private final String resourceName; + private final @Nullable String resourceName; /** 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 2bfefa1c404c..c1228a056979 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 @@ -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,12 +41,12 @@ import kotlin.reflect.jvm.ReflectJvmMapping; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.KotlinDetector; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.io.UrlResource; import org.springframework.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -104,8 +104,7 @@ public class SpringFactoriesLoader { static final Map> cache = new ConcurrentReferenceHashMap<>(); - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final Map> factories; @@ -126,13 +125,12 @@ protected SpringFactoriesLoader(@Nullable ClassLoader classLoader, MapThe 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 @@ -146,10 +144,9 @@ public List load(Class factoryType) { * 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 @@ -164,10 +161,9 @@ public List load(Class factoryType, @Nullable ArgumentResolver argumen * 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 @@ -183,10 +179,9 @@ public List load(Class factoryType, @Nullable FailureHandler failureHa * {@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 @@ -216,8 +211,7 @@ private List loadFactoryNames(Class factoryType) { return this.factories.getOrDefault(factoryType.getName(), Collections.emptyList()); } - @Nullable - protected T instantiateFactory(String implementationName, Class type, + protected @Nullable T instantiateFactory(String implementationName, Class type, @Nullable ArgumentResolver argumentResolver, FailureHandler failureHandler) { try { @@ -237,12 +231,11 @@ protected T instantiateFactory(String implementationName, Class type, /** * 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} @@ -258,9 +251,8 @@ public static List loadFactories(Class factoryType, @Nullable ClassLoa * 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 @@ -376,7 +368,7 @@ private FactoryInstantiator(Constructor constructor) { T instantiate(@Nullable ArgumentResolver argumentResolver) throws Exception { Object[] args = resolveArgs(argumentResolver); - if (isKotlinType(this.constructor.getDeclaringClass())) { + if (KotlinDetector.isKotlinType(this.constructor.getDeclaringClass())) { return KotlinDelegate.instantiate(this.constructor, args); } return this.constructor.newInstance(args); @@ -397,8 +389,7 @@ static FactoryInstantiator forClass(Class factoryImplementationClass) return new FactoryInstantiator<>((Constructor) constructor); } - @Nullable - private static Constructor findConstructor(Class factoryImplementationClass) { + private static @Nullable Constructor findConstructor(Class factoryImplementationClass) { // Same algorithm as BeanUtils.getResolvableConstructor Constructor constructor = findPrimaryKotlinConstructor(factoryImplementationClass); constructor = (constructor != null ? constructor : @@ -410,23 +401,16 @@ private static Constructor findConstructor(Class factoryImplementationClas return constructor; } - @Nullable - private static Constructor findPrimaryKotlinConstructor(Class factoryImplementationClass) { - return (isKotlinType(factoryImplementationClass) ? + private static @Nullable Constructor findPrimaryKotlinConstructor(Class factoryImplementationClass) { + return (KotlinDetector.isKotlinType(factoryImplementationClass) ? KotlinDelegate.findPrimaryConstructor(factoryImplementationClass) : null); } - private static boolean isKotlinType(Class factoryImplementationClass) { - return KotlinDetector.isKotlinReflectPresent() && KotlinDetector.isKotlinType(factoryImplementationClass); - } - - @Nullable - private static Constructor findSingleConstructor(Constructor[] constructors) { + private static @Nullable Constructor findSingleConstructor(Constructor[] constructors) { return (constructors.length == 1 ? constructors[0] : null); } - @Nullable - private static Constructor findDeclaredConstructor(Class factoryImplementationClass) { + private static @Nullable Constructor findDeclaredConstructor(Class factoryImplementationClass) { try { return factoryImplementationClass.getDeclaredConstructor(); } @@ -443,8 +427,7 @@ private static Constructor findDeclaredConstructor(Class factoryImplementa */ private static class KotlinDelegate { - @Nullable - static Constructor findPrimaryConstructor(Class clazz) { + static @Nullable Constructor findPrimaryConstructor(Class clazz) { try { KFunction primaryConstructor = KClasses.getPrimaryConstructor(JvmClassMappingKt.getKotlinClass(clazz)); if (primaryConstructor != null) { @@ -472,8 +455,8 @@ static T instantiate(Constructor constructor, Object[] args) throws Excep private static void makeAccessible(Constructor constructor, KFunction kotlinConstructor) { - if ((!Modifier.isPublic(constructor.getModifiers()) - || !Modifier.isPublic(constructor.getDeclaringClass().getModifiers()))) { + if ((!Modifier.isPublic(constructor.getModifiers()) || + !Modifier.isPublic(constructor.getDeclaringClass().getModifiers()))) { KCallablesJvm.setAccessible(kotlinConstructor, true); } } @@ -512,8 +495,7 @@ public interface ArgumentResolver { * @param type the argument type * @return the resolved argument value or {@code null} */ - @Nullable - T resolve(Class type); + @Nullable T resolve(Class type); /** * Create a new composed {@link ArgumentResolver} by combining this resolver @@ -592,11 +574,11 @@ static ArgumentResolver ofSupplied(Class type, Supplier valueSupplier) * @param function the resolver function * @return a new {@link ArgumentResolver} instance backed by the function */ - static ArgumentResolver from(Function, Object> function) { + static ArgumentResolver from(Function, @Nullable Object> function) { return new ArgumentResolver() { @SuppressWarnings("unchecked") @Override - public T resolve(Class type) { + public @Nullable T resolve(Class type) { return (T) function.apply(type); } }; diff --git a/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java b/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java index 5fcaf8af34dc..767ed0796716 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/VfsPatternUtils.java @@ -21,8 +21,9 @@ import java.lang.reflect.Proxy; import java.net.URL; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.VfsUtils; -import org.springframework.lang.Nullable; /** * Artificial class used for accessing the {@link VfsUtils} methods @@ -33,8 +34,7 @@ */ abstract class VfsPatternUtils extends VfsUtils { - @Nullable - static Object getVisitorAttributes() { + static @Nullable Object getVisitorAttributes() { return doGetVisitorAttributes(); } diff --git a/spring-core/src/main/java/org/springframework/core/io/support/package-info.java b/spring-core/src/main/java/org/springframework/core/io/support/package-info.java index 2b1395fbb2eb..36169e1f5149 100644 --- a/spring-core/src/main/java/org/springframework/core/io/support/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/io/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for Spring's resource abstraction. * Includes a ResourcePatternResolver mechanism. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.io.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java b/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java index 588b9a063209..0937dd6c8c4f 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogDelegateFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,9 +26,7 @@ /** * Factory for common {@link Log} delegates with Spring's logging conventions. * - *

    Mainly for internal use within the framework with Apache Commons Logging, - * typically in the form of the {@code spring-jcl} bridge but also compatible - * with other Commons Logging bridges. + *

    Mainly for internal use within the framework with Apache Commons Logging. * * @author Rossen Stoyanchev * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java index 786680bb2a3a..6ddce2c76d92 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogFormatUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,17 +20,15 @@ import java.util.regex.Pattern; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; /** * Utility methods for formatting and logging messages. * - *

    Mainly for internal use within the framework with Apache Commons Logging, - * typically in the form of the {@code spring-jcl} bridge but also compatible - * with other Commons Logging bridges. + *

    Mainly for internal use within the framework with Apache Commons Logging. * * @author Rossen Stoyanchev * @author Juergen Hoeller diff --git a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java index e1206d5c3530..12669dc7eff5 100644 --- a/spring-core/src/main/java/org/springframework/core/log/LogMessage.java +++ b/spring-core/src/main/java/org/springframework/core/log/LogMessage.java @@ -18,7 +18,8 @@ import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -42,8 +43,7 @@ */ public abstract class LogMessage implements CharSequence { - @Nullable - private String result; + private @Nullable String result; @Override @@ -176,8 +176,7 @@ private abstract static class FormatMessage extends LogMessage { private static final class FormatMessage1 extends FormatMessage { - @Nullable - private final Object arg1; + private final @Nullable Object arg1; FormatMessage1(String format, @Nullable Object arg1) { super(format); @@ -193,11 +192,9 @@ protected String buildString() { private static final class FormatMessage2 extends FormatMessage { - @Nullable - private final Object arg1; + private final @Nullable Object arg1; - @Nullable - private final Object arg2; + private final @Nullable Object arg2; FormatMessage2(String format, @Nullable Object arg1, @Nullable Object arg2) { super(format); @@ -214,14 +211,11 @@ String buildString() { private static final class FormatMessage3 extends FormatMessage { - @Nullable - private final Object arg1; + private final @Nullable Object arg1; - @Nullable - private final Object arg2; + private final @Nullable Object arg2; - @Nullable - private final Object arg3; + private final @Nullable Object arg3; FormatMessage3(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3) { super(format); @@ -239,17 +233,13 @@ String buildString() { private static final class FormatMessage4 extends FormatMessage { - @Nullable - private final Object arg1; + private final @Nullable Object arg1; - @Nullable - private final Object arg2; + private final @Nullable Object arg2; - @Nullable - private final Object arg3; + private final @Nullable Object arg3; - @Nullable - private final Object arg4; + private final @Nullable Object arg4; FormatMessage4(String format, @Nullable Object arg1, @Nullable Object arg2, @Nullable Object arg3, @Nullable Object arg4) { @@ -269,8 +259,7 @@ String buildString() { private static final class FormatMessageX extends FormatMessage { - @Nullable - private final Object[] args; + private final @Nullable Object[] args; FormatMessageX(String format, @Nullable Object... args) { super(format); diff --git a/spring-core/src/main/java/org/springframework/core/log/package-info.java b/spring-core/src/main/java/org/springframework/core/log/package-info.java index b14fac177c63..eb628ffa883c 100644 --- a/spring-core/src/main/java/org/springframework/core/log/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/log/package-info.java @@ -1,9 +1,7 @@ /** * Useful delegates for Spring's logging conventions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.log; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java index 9a497d3eb9ee..b32263a01b50 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/DefaultApplicationStartup.java @@ -20,7 +20,7 @@ import java.util.Iterator; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Default "no op" {@code ApplicationStartup} implementation. @@ -54,8 +54,7 @@ public long getId() { } @Override - @Nullable - public Long getParentId() { + public @Nullable Long getParentId() { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java b/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java index e9061cb4235d..2f35beefdc58 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/StartupStep.java @@ -18,7 +18,7 @@ import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Step recording metrics about a particular phase or action happening during the {@link ApplicationStartup}. @@ -56,8 +56,7 @@ public interface StartupStep { *

    The parent step is the step that was started the most recently * when the current step was created. */ - @Nullable - Long getParentId(); + @Nullable Long getParentId(); /** * Add a {@link Tag} to the step. diff --git a/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java b/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java index 54b7c8db1137..b005cb230b78 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/jfr/package-info.java @@ -1,9 +1,7 @@ /** * Support package for recording startup metrics using Java Flight Recorder. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.metrics.jfr; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/metrics/package-info.java b/spring-core/src/main/java/org/springframework/core/metrics/package-info.java index 8774a4bd1113..c4deb39d6a95 100644 --- a/spring-core/src/main/java/org/springframework/core/metrics/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/metrics/package-info.java @@ -1,9 +1,7 @@ /** * Support package for recording metrics during application startup. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.metrics; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/package-info.java b/spring-core/src/main/java/org/springframework/core/package-info.java index 703f29b4b238..b347e187ed14 100644 --- a/spring-core/src/main/java/org/springframework/core/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/package-info.java @@ -2,9 +2,7 @@ * Provides basic classes for exception handling and version detection, * and other core helpers that are not specific to any part of the framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/retry/RetryCallback.java b/spring-core/src/main/java/org/springframework/core/retry/RetryCallback.java new file mode 100644 index 000000000000..05463440a199 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryCallback.java @@ -0,0 +1,49 @@ +/* + * Copyright 2002-2025 the original author 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; + +/** + * Callback interface for a retryable block of code. + * + *

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

    Defaults to the fully-qualified class name. + * @return the name of the callback + */ + default String getName() { + return getClass().getName(); + } + +} 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 000000000000..93c46c19f36e --- /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 000000000000..f021911cd7d6 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryExecution.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.core.retry; + +/** + * Strategy interface to define a retry execution created for a given + * {@link 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 000000000000..9586e778fd6a --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryListener.java @@ -0,0 +1,64 @@ +/* + * Copyright 2002-2025 the original author 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.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 callback + */ + default void onRetrySuccess(RetryExecution retryExecution, Object result) { + } + + /** + * Called every time a retry attempt fails. + * @param retryExecution the retry execution + * @param throwable the exception thrown by the callback + */ + 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 RetryCallback} + */ + 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 000000000000..a731b8a3571b --- /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 callback (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 retryCallback the callback to call initially and retry if needed + * @param the type of the result + * @return the result of the callback, if any + * @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions + * encountered during retry attempts should be made available as suppressed + * exceptions + */ + R execute(RetryCallback retryCallback) 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 000000000000..d7335fb9143d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java @@ -0,0 +1,33 @@ +/* + * Copyright 2002-2025 the original author 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 policy. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +public interface RetryPolicy { + + /** + * Start a new execution for this retry policy. + * @return a new {@link RetryExecution} + */ + RetryExecution start(); + +} 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 000000000000..1285fe84801d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java @@ -0,0 +1,196 @@ +/* + * Copyright 2002-2025 the original author 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.ArrayList; +import java.util.List; + +import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.log.LogAccessor; +import org.springframework.core.retry.support.CompositeRetryListener; +import org.springframework.core.retry.support.MaxRetryAttemptsPolicy; +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 invokes and potentially + * retries a {@link RetryCallback} based on a configured {@link RetryPolicy} and + * {@link BackOff} policy. + * + *

    By default, a callback will be invoked 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 operations performed by this class are logged at debug level, + * using {@code "org.springframework.core.retry.RetryTemplate"} as the log category. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + * @see RetryOperations + * @see RetryPolicy + * @see BackOff + * @see RetryListener + * @see RetryCallback + */ +public class RetryTemplate implements RetryOperations { + + protected final LogAccessor logger = new LogAccessor(LogFactory.getLog(getClass())); + + protected RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy(); + + protected BackOff backOffPolicy = new FixedBackOff(1000, Long.MAX_VALUE); + + protected 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 new MaxRetryAttemptsPolicy()}. + * @param retryPolicy the retry policy to use + * @see MaxRetryAttemptsPolicy + */ + 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(1000, Long.MAX_VALUE))}. + * @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 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 RetryCallback} according to the configured + * retry and backoff policies. + *

    If the callback succeeds, its result will be returned. Otherwise, a + * {@link RetryException} will be thrown to the caller. + * @param retryCallback the callback to call initially and retry if needed + * @param the type of the result + * @return the result of the callback, if any + * @throws RetryException if the {@code RetryPolicy} is exhausted; exceptions + * encountered during retry attempts are available as suppressed exceptions + */ + @Override + public R execute(RetryCallback retryCallback) throws RetryException { + String callbackName = retryCallback.getName(); + // Initial attempt + try { + logger.debug(() -> "Preparing to execute callback '" + callbackName + "'"); + R result = retryCallback.run(); + logger.debug(() -> "Callback '" + callbackName + "' completed successfully"); + return result; + } + catch (Throwable initialException) { + logger.debug(initialException, + () -> "Execution of callback '" + callbackName + "' failed; initiating the retry process"); + // Retry process starts here + RetryExecution retryExecution = this.retryPolicy.start(); + BackOffExecution backOffExecution = this.backOffPolicy.start(); + List suppressedExceptions = new ArrayList<>(); + + Throwable retryException = initialException; + while (retryExecution.shouldRetry(retryException)) { + logger.debug(() -> "Preparing to retry callback '" + callbackName + "'"); + try { + this.retryListener.beforeRetry(retryExecution); + R result = retryCallback.run(); + this.retryListener.onRetrySuccess(retryExecution, result); + logger.debug(() -> "Callback '" + callbackName + "' completed successfully after retry"); + return result; + } + catch (Throwable currentAttemptException) { + this.retryListener.onRetryFailure(retryExecution, currentAttemptException); + try { + long duration = backOffExecution.nextBackOff(); + logger.debug(() -> "Retry callback '" + callbackName + "' failed due to '" + + currentAttemptException.getMessage() + "'; backing off for " + duration + "ms"); + Thread.sleep(duration); + } + catch (InterruptedException interruptedException) { + Thread.currentThread().interrupt(); + throw new RetryException("Unable to back off for retry callback '" + callbackName + "'", + interruptedException); + } + suppressedExceptions.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 callback '" + callbackName + + "' exhausted; aborting execution", initialException); + suppressedExceptions.forEach(finalException::addSuppressed); + this.retryListener.onRetryPolicyExhaustion(retryExecution, finalException); + throw finalException; + } + } + +} 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 000000000000..9c7f8598c8e2 --- /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 000000000000..f97a04125b9d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java @@ -0,0 +1,86 @@ +/* + * Copyright 2002-2025 the original author 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.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. + * + *

    This class is used to compose multiple listeners within a {@link RetryTemplate}. + * + *

    Delegate listeners will be called in their registration order. + * + * @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, 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/MaxRetryAttemptsPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java new file mode 100644 index 000000000000..1e82bf1c66fc --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.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 org.springframework.core.retry.RetryExecution; +import org.springframework.core.retry.RetryPolicy; +import org.springframework.util.Assert; + +/** + * A {@link RetryPolicy} based on a number of attempts that should not exceed a + * configured maximum number. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +public class MaxRetryAttemptsPolicy implements RetryPolicy { + + /** + * The default maximum number of retry attempts: {@value}. + */ + public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 3; + + + private int maxRetryAttempts = DEFAULT_MAX_RETRY_ATTEMPTS; + + + /** + * Create a new {@code MaxRetryAttemptsPolicy} with the default maximum number + * of retry attempts. + * @see #DEFAULT_MAX_RETRY_ATTEMPTS + */ + public MaxRetryAttemptsPolicy() { + } + + /** + * Create a new {@code MaxRetryAttemptsPolicy} with the specified maximum number + * of retry attempts. + * @param maxRetryAttempts the maximum number of retry attempts; must be greater + * than zero + */ + public MaxRetryAttemptsPolicy(int maxRetryAttempts) { + setMaxRetryAttempts(maxRetryAttempts); + } + + @Override + public RetryExecution start() { + return new MaxRetryAttemptsPolicyExecution(); + } + + /** + * Set the maximum number of retry attempts. + * @param maxRetryAttempts the maximum number of retry attempts; must be greater + * than zero + */ + public void setMaxRetryAttempts(int maxRetryAttempts) { + Assert.isTrue(maxRetryAttempts > 0, "Max retry attempts must be greater than zero"); + this.maxRetryAttempts = maxRetryAttempts; + } + + + /** + * A {@link RetryExecution} based on a maximum number of retry attempts. + */ + private class MaxRetryAttemptsPolicyExecution implements RetryExecution { + + private int retryAttempts; + + @Override + public boolean shouldRetry(Throwable throwable) { + return (this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts); + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java new file mode 100644 index 000000000000..bbd32d4bb5df --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java @@ -0,0 +1,89 @@ +/* + * Copyright 2002-2025 the original author 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.time.Duration; +import java.time.LocalDateTime; + +import org.springframework.core.retry.RetryExecution; +import org.springframework.core.retry.RetryPolicy; +import org.springframework.util.Assert; + +/** + * A {@link RetryPolicy} based on a maximum retry duration. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +public class MaxRetryDurationPolicy implements RetryPolicy { + + /** + * The default maximum retry duration: 3 seconds. + */ + public static final Duration DEFAULT_MAX_RETRY_DURATION = Duration.ofSeconds(3); + + + private Duration maxRetryDuration = DEFAULT_MAX_RETRY_DURATION; + + + /** + * Create a new {@code MaxRetryDurationPolicy} with the default maximum retry + * duration. + * @see #DEFAULT_MAX_RETRY_DURATION + */ + public MaxRetryDurationPolicy() { + } + + /** + * Create a new {@code MaxRetryDurationPolicy} with the specified maximum retry + * duration. + * @param maxRetryDuration the maximum retry duration; must be positive + */ + public MaxRetryDurationPolicy(Duration maxRetryDuration) { + setMaxRetryDuration(maxRetryDuration); + } + + @Override + public RetryExecution start() { + return new MaxRetryDurationPolicyExecution(); + } + + /** + * Set the maximum retry duration. + * @param maxRetryDuration the maximum retry duration; must be positive + */ + public void setMaxRetryDuration(Duration maxRetryDuration) { + Assert.isTrue(!maxRetryDuration.isNegative() && !maxRetryDuration.isZero(), + "Max retry duration must be positive"); + this.maxRetryDuration = maxRetryDuration; + } + + /** + * A {@link RetryExecution} based on a maximum retry duration. + */ + private class MaxRetryDurationPolicyExecution implements RetryExecution { + + private final LocalDateTime retryStartTime = LocalDateTime.now(); + + @Override + public boolean shouldRetry(Throwable throwable) { + Duration currentRetryDuration = Duration.between(this.retryStartTime, LocalDateTime.now()); + return currentRetryDuration.compareTo(MaxRetryDurationPolicy.this.maxRetryDuration) <= 0; + } + } + +} diff --git a/spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java b/spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java new file mode 100644 index 000000000000..afe186a5e9ee --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java @@ -0,0 +1,50 @@ +/* + * Copyright 2002-2025 the original author 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.function.Predicate; + +import org.springframework.core.retry.RetryExecution; +import org.springframework.core.retry.RetryPolicy; + +/** + * A {@link RetryPolicy} based on a {@link Predicate}. + * + * @author Mahmoud Ben Hassine + * @since 7.0 + */ +public class PredicateRetryPolicy implements RetryPolicy { + + private final Predicate predicate; + + + /** + * Create a new {@code PredicateRetryPolicy} with the given predicate. + * @param predicate the predicate to use for determining whether to retry an + * operation based on a given {@link Throwable} + */ + public PredicateRetryPolicy(Predicate predicate) { + this.predicate = predicate; + } + + + @Override + public RetryExecution start() { + return this.predicate::test; + } + +} 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 000000000000..598666fab6bd --- /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 containing common retry policies. + */ +@NullMarked +package org.springframework.core.retry.support; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java index 3096daf9611f..9a642fe4c82e 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/DefaultDeserializer.java @@ -20,8 +20,9 @@ import java.io.InputStream; import java.io.ObjectInputStream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ConfigurableObjectInputStream; -import org.springframework.lang.Nullable; /** * A default {@link Deserializer} implementation that reads an input stream @@ -35,8 +36,7 @@ */ public class DefaultDeserializer implements Deserializer { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; /** diff --git a/spring-core/src/main/java/org/springframework/core/serializer/package-info.java b/spring-core/src/main/java/org/springframework/core/serializer/package-info.java index 88f4d3ba4a36..f3d5b1780242 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/package-info.java @@ -3,9 +3,7 @@ * Provides an abstraction over various serialization techniques. * Includes exceptions for serialization and deserialization failures. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.serializer; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java index 782ddc901bd5..c8accc6e3893 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/DeserializingConverter.java @@ -18,10 +18,11 @@ import java.io.ByteArrayInputStream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.converter.Converter; import org.springframework.core.serializer.DefaultDeserializer; import org.springframework.core.serializer.Deserializer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java b/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java index 4e46ea8a5921..499d6224ab8d 100644 --- a/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/serializer/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for Spring's serializer abstraction. * Includes adapters to the Converter SPI. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.serializer.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java index 9eb0d37205fe..5ad1e93a59e3 100644 --- a/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultToStringStyler.java @@ -16,7 +16,8 @@ package org.springframework.core.style; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java index 3dd8891646b8..50548a6d1771 100644 --- a/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java +++ b/spring-core/src/main/java/org/springframework/core/style/DefaultValueStyler.java @@ -23,7 +23,8 @@ import java.util.Set; import java.util.StringJoiner; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; diff --git a/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java index fbb5cc96bd18..d857f8598279 100644 --- a/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringCreator.java @@ -16,7 +16,8 @@ package org.springframework.core.style; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java b/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java index 3e20e72d94a1..06d3793e9de0 100644 --- a/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java +++ b/spring-core/src/main/java/org/springframework/core/style/ToStringStyler.java @@ -16,7 +16,7 @@ package org.springframework.core.style; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A strategy interface for pretty-printing {@code toString()} methods. diff --git a/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java b/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java index 974a72d551dd..a2e21f5a9576 100644 --- a/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java +++ b/spring-core/src/main/java/org/springframework/core/style/ValueStyler.java @@ -16,7 +16,7 @@ package org.springframework.core.style; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy that encapsulates value String styling algorithms diff --git a/spring-core/src/main/java/org/springframework/core/style/package-info.java b/spring-core/src/main/java/org/springframework/core/style/package-info.java index c3fe9bfa482d..7bd028d63c91 100644 --- a/spring-core/src/main/java/org/springframework/core/style/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/style/package-info.java @@ -1,9 +1,7 @@ /** * Support for styling values as Strings, with ToStringCreator as central class. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.style; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java b/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java deleted file mode 100644 index a89a7efd126b..000000000000 --- a/spring-core/src/main/java/org/springframework/core/task/AsyncListenableTaskExecutor.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.core.task; - -import java.util.concurrent.Callable; - -/** - * Extension of the {@link AsyncTaskExecutor} interface, adding the capability to submit - * tasks for {@code ListenableFutures}. - * - * @author Arjen Poutsma - * @since 4.0 - * @deprecated as of 6.0, in favor of - * {@link AsyncTaskExecutor#submitCompletable(Runnable)} and - * {@link AsyncTaskExecutor#submitCompletable(Callable)} - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public interface AsyncListenableTaskExecutor extends AsyncTaskExecutor { - - /** - * Submit a {@code Runnable} task for execution, receiving a {@code ListenableFuture} - * representing that task. The Future will return a {@code null} result upon completion. - * @param task the {@code Runnable} to execute (never {@code null}) - * @return a {@code ListenableFuture} representing pending completion of the task - * @throws TaskRejectedException if the given task was not accepted - * @deprecated in favor of {@link AsyncTaskExecutor#submitCompletable(Runnable)} - */ - @Deprecated(since = "6.0", forRemoval = true) - org.springframework.util.concurrent.ListenableFuture submitListenable(Runnable task); - - /** - * Submit a {@code Callable} task for execution, receiving a {@code ListenableFuture} - * representing that task. The Future will return the Callable's result upon - * completion. - * @param task the {@code Callable} to execute (never {@code null}) - * @return a {@code ListenableFuture} representing pending completion of the task - * @throws TaskRejectedException if the given task was not accepted - * @deprecated in favor of {@link AsyncTaskExecutor#submitCompletable(Callable)} - */ - @Deprecated(since = "6.0", forRemoval = true) - org.springframework.util.concurrent.ListenableFuture submitListenable(Callable task); - -} 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 39cc0b5cb651..d4e7268f3e5d 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 e2d2363373fb..d34a6bf10780 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. @@ -24,12 +24,11 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.ThreadFactory; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ConcurrencyThrottleSupport; import org.springframework.util.CustomizableThreadCreator; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; /** * {@link TaskExecutor} implementation that fires up a new Thread for each task, @@ -58,9 +57,9 @@ * @see org.springframework.scheduling.concurrent.SimpleAsyncTaskScheduler * @see org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor */ -@SuppressWarnings({"serial", "removal"}) +@SuppressWarnings("serial") public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator - implements AsyncListenableTaskExecutor, Serializable, AutoCloseable { + implements AsyncTaskExecutor, Serializable, AutoCloseable { /** * Permit any number of concurrent invocations: that is, don't throttle concurrency. @@ -78,19 +77,17 @@ public class SimpleAsyncTaskExecutor extends CustomizableThreadCreator /** Internal concurrency throttle used by this executor. */ private final ConcurrencyThrottleAdapter concurrencyThrottle = new ConcurrencyThrottleAdapter(); - @Nullable - private VirtualThreadDelegate virtualThreadDelegate; + private @Nullable VirtualThreadDelegate virtualThreadDelegate; - @Nullable - private ThreadFactory threadFactory; + private @Nullable ThreadFactory threadFactory; - @Nullable - private TaskDecorator taskDecorator; + private @Nullable TaskDecorator taskDecorator; private long taskTerminationTimeout; - @Nullable - private Set activeThreads; + private @Nullable Set activeThreads; + + private boolean rejectTasksWhenLimitReached = false; private volatile boolean active = true; @@ -144,8 +141,7 @@ public void setThreadFactory(@Nullable ThreadFactory threadFactory) { /** * Return the external factory to use for creating new Threads, if any. */ - @Nullable - public final ThreadFactory getThreadFactory() { + public final @Nullable ThreadFactory getThreadFactory() { return this.threadFactory; } @@ -190,6 +186,17 @@ public void setTaskTerminationTimeout(long timeout) { 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. @@ -257,7 +264,7 @@ public void execute(Runnable task) { * @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"); @@ -294,22 +301,6 @@ public Future submit(Callable task) { return future; } - @SuppressWarnings("deprecation") - @Override - public ListenableFuture submitListenable(Runnable task) { - ListenableFutureTask future = new ListenableFutureTask<>(task, null); - execute(future, TIMEOUT_INDEFINITE); - return future; - } - - @SuppressWarnings("deprecation") - @Override - public ListenableFuture submitListenable(Callable task) { - ListenableFutureTask future = new ListenableFutureTask<>(task); - execute(future, TIMEOUT_INDEFINITE); - return future; - } - /** * Template method for the actual execution of a task. *

    The default implementation creates a new Thread and starts it. @@ -372,13 +363,21 @@ public void close() { * 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 ddec24acb697..fc03e1d92d51 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 @@ * @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/task/package-info.java b/spring-core/src/main/java/org/springframework/core/task/package-info.java index 099867d9c64f..367345a7175a 100644 --- a/spring-core/src/main/java/org/springframework/core/task/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/task/package-info.java @@ -2,9 +2,7 @@ * This package defines Spring's core TaskExecutor abstraction, * and provides SyncTaskExecutor and SimpleAsyncTaskExecutor implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.task; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java index 0669f7ea7c81..fd9788e35c6a 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/TaskExecutorAdapter.java @@ -23,13 +23,12 @@ import java.util.concurrent.FutureTask; import java.util.concurrent.RejectedExecutionException; -import org.springframework.core.task.AsyncListenableTaskExecutor; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.task.TaskDecorator; import org.springframework.core.task.TaskRejectedException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; -import org.springframework.util.concurrent.ListenableFuture; -import org.springframework.util.concurrent.ListenableFutureTask; /** * Adapter that takes a JDK {@code java.util.concurrent.Executor} and @@ -43,13 +42,12 @@ * @see java.util.concurrent.ExecutorService * @see java.util.concurrent.Executors */ -@SuppressWarnings({"deprecation", "removal"}) -public class TaskExecutorAdapter implements AsyncListenableTaskExecutor { +@SuppressWarnings("deprecation") +public class TaskExecutorAdapter implements AsyncTaskExecutor { private final Executor concurrentExecutor; - @Nullable - private TaskDecorator taskDecorator; + private @Nullable TaskDecorator taskDecorator; /** @@ -133,30 +131,6 @@ public Future submit(Callable task) { } } - @Override - public ListenableFuture submitListenable(Runnable task) { - try { - ListenableFutureTask future = new ListenableFutureTask<>(task, null); - doExecute(this.concurrentExecutor, this.taskDecorator, future); - return future; - } - catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.concurrentExecutor, task, ex); - } - } - - @Override - public ListenableFuture submitListenable(Callable task) { - try { - ListenableFutureTask future = new ListenableFutureTask<>(task); - doExecute(this.concurrentExecutor, this.taskDecorator, future); - return future; - } - catch (RejectedExecutionException ex) { - throw new TaskRejectedException(this.concurrentExecutor, task, ex); - } - } - /** * Actually execute the given {@code Runnable} (which may be a user-supplied task diff --git a/spring-core/src/main/java/org/springframework/core/task/support/package-info.java b/spring-core/src/main/java/org/springframework/core/task/support/package-info.java index e9dfcd9dc443..9eb9c5293770 100644 --- a/spring-core/src/main/java/org/springframework/core/task/support/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/task/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for Spring's TaskExecutor abstraction. * Includes an adapter for the standard ExecutorService interface. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.task.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java index f5608ffc346c..5957199547b4 100644 --- a/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/AnnotatedTypeMetadata.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. @@ -25,6 +25,8 @@ import java.util.stream.Collectors; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotation.Adapt; @@ -32,7 +34,6 @@ import org.springframework.core.annotation.MergedAnnotationPredicates; import org.springframework.core.annotation.MergedAnnotationSelectors; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; /** @@ -86,8 +87,7 @@ default boolean isAnnotated(String annotationName) { * as map key (for example, "location") and the attribute's value as map value; or * {@code null} if no matching annotation is found */ - @Nullable - default Map getAnnotationAttributes(String annotationName) { + default @Nullable Map getAnnotationAttributes(String annotationName) { return getAnnotationAttributes(annotationName, false); } @@ -106,8 +106,7 @@ default Map getAnnotationAttributes(String annotationName) { * as map key (for example, "location") and the attribute's value as map value; or * {@code null} if no matching annotation is found */ - @Nullable - default Map getAnnotationAttributes(String annotationName, + default @Nullable Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { MergedAnnotation annotation = getAnnotations().get(annotationName, @@ -130,8 +129,7 @@ default Map getAnnotationAttributes(String annotationName, * map value; or {@code null} if no matching annotation is found * @see #getAllAnnotationAttributes(String, boolean) */ - @Nullable - default MultiValueMap getAllAnnotationAttributes(String annotationName) { + default @Nullable MultiValueMap getAllAnnotationAttributes(String annotationName) { return getAllAnnotationAttributes(annotationName, false); } @@ -150,8 +148,7 @@ default MultiValueMap getAllAnnotationAttributes(String annotati * map value; or {@code null} if no matching annotation is found * @see #getAllAnnotationAttributes(String) */ - @Nullable - default MultiValueMap getAllAnnotationAttributes( + default @Nullable MultiValueMap getAllAnnotationAttributes( String annotationName, boolean classValuesAsString) { Adapt[] adaptations = Adapt.values(classValuesAsString, true); @@ -159,7 +156,7 @@ default MultiValueMap getAllAnnotationAttributes( .filter(MergedAnnotationPredicates.unique(MergedAnnotation::getMetaTypes)) .map(MergedAnnotation::withNonMergedAttributes) .collect(MergedAnnotationCollectors.toMultiValueMap( - map -> (map.isEmpty() ? null : map), adaptations)); + (MultiValueMap map) -> (map.isEmpty() ? null : map), adaptations)); } /** diff --git a/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java b/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java index cbb5ab4168fe..96ab62f0220d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/ClassMetadata.java @@ -16,7 +16,7 @@ package org.springframework.core.type; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that defines abstract metadata of a specific class, @@ -86,8 +86,7 @@ default boolean hasEnclosingClass() { * Return the name of the enclosing class of the underlying class, * or {@code null} if the underlying class is a top-level class. */ - @Nullable - String getEnclosingClassName(); + @Nullable String getEnclosingClassName(); /** * Return whether the underlying class has a superclass. @@ -100,8 +99,7 @@ default boolean hasSuperClass() { * Return the name of the superclass of the underlying class, * or {@code null} if there is no superclass defined. */ - @Nullable - String getSuperClassName(); + @Nullable String getSuperClassName(); /** * Return the names of all interfaces that the underlying class 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 bc34ed937a51..9bf5b5eac937 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 @@ -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. @@ -23,13 +23,14 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.RepeatableContainers; -import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; @@ -50,17 +51,16 @@ public class StandardAnnotationMetadata extends StandardClassMetadata implements private final boolean nestedAnnotationsAsMap; - @Nullable - private Set annotationTypes; + private @Nullable Set annotationTypes; /** * 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 StandardAnnotationMetadata(Class introspectedClass) { * {@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, @@ -105,8 +105,7 @@ public Set getAnnotationTypes() { } @Override - @Nullable - public Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { + public @Nullable Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { if (this.nestedAnnotationsAsMap) { return AnnotationMetadata.super.getAnnotationAttributes(annotationName, classValuesAsString); } @@ -115,8 +114,8 @@ public Map getAnnotationAttributes(String annotationName, boolea } @Override - @Nullable - public MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { + @SuppressWarnings("NullAway") // Null-safety of Java super method not yet managed + public @Nullable MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { if (this.nestedAnnotationsAsMap) { return AnnotationMetadata.super.getAllAnnotationAttributes(annotationName, classValuesAsString); } 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 702b5c1b7744..86c47696d447 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. @@ -19,7 +19,8 @@ import java.lang.reflect.Modifier; import java.util.LinkedHashSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -39,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; @@ -88,15 +89,13 @@ public boolean isIndependent() { } @Override - @Nullable - public String getEnclosingClassName() { + public @Nullable String getEnclosingClassName() { Class enclosingClass = this.introspectedClass.getEnclosingClass(); return (enclosingClass != null ? enclosingClass.getName() : null); } @Override - @Nullable - public String getSuperClassName() { + public @Nullable String getSuperClassName() { Class superClass = this.introspectedClass.getSuperclass(); return (superClass != null ? superClass.getName() : null); } 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 40943a223f31..4dedaac96eb2 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 @@ -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,11 +20,12 @@ import java.lang.reflect.Modifier; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.RepeatableContainers; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -51,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); } @@ -130,8 +131,7 @@ private boolean isPrivate() { } @Override - @Nullable - public Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { + public @Nullable Map getAnnotationAttributes(String annotationName, boolean classValuesAsString) { if (this.nestedAnnotationsAsMap) { return MethodMetadata.super.getAnnotationAttributes(annotationName, classValuesAsString); } @@ -140,8 +140,8 @@ public Map getAnnotationAttributes(String annotationName, boolea } @Override - @Nullable - public MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { + @SuppressWarnings("NullAway") // Null-safety of Java super method not yet managed + public @Nullable MultiValueMap getAllAnnotationAttributes(String annotationName, boolean classValuesAsString) { if (this.nestedAnnotationsAsMap) { return MethodMetadata.super.getAllAnnotationAttributes(annotationName, classValuesAsString); } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java index 9d9956fcf26a..d46a3ff7e861 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/CachingMetadataReaderFactory.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. @@ -21,10 +21,11 @@ import java.util.Map; import java.util.concurrent.ConcurrentMap; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; /** * Caching implementation of the {@link MetadataReaderFactory} interface, @@ -35,14 +36,15 @@ * @author Costin Leau * @since 2.5 */ -public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { +public class CachingMetadataReaderFactory implements MetadataReaderFactory { /** Default maximum number of entries for a local MetadataReader cache: 256. */ public static final int DEFAULT_CACHE_LIMIT = 256; + private final MetadataReaderFactory delegate; + /** MetadataReader cache: either local or shared at the ResourceLoader level. */ - @Nullable - private Map metadataReaderCache; + private @Nullable Map metadataReaderCache; /** @@ -50,7 +52,7 @@ public class CachingMetadataReaderFactory extends SimpleMetadataReaderFactory { * using a local resource cache. */ public CachingMetadataReaderFactory() { - super(); + this.delegate = MetadataReaderFactory.create((ClassLoader) null); setCacheLimit(DEFAULT_CACHE_LIMIT); } @@ -60,7 +62,7 @@ public CachingMetadataReaderFactory() { * @param classLoader the ClassLoader to use */ public CachingMetadataReaderFactory(@Nullable ClassLoader classLoader) { - super(classLoader); + this.delegate = MetadataReaderFactory.create(classLoader); setCacheLimit(DEFAULT_CACHE_LIMIT); } @@ -72,7 +74,7 @@ public CachingMetadataReaderFactory(@Nullable ClassLoader classLoader) { * @see DefaultResourceLoader#getResourceCache */ public CachingMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { - super(resourceLoader); + this.delegate = MetadataReaderFactory.create(resourceLoader); if (resourceLoader instanceof DefaultResourceLoader defaultResourceLoader) { this.metadataReaderCache = defaultResourceLoader.getResourceCache(MetadataReader.class); } @@ -81,7 +83,6 @@ public CachingMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { } } - /** * Specify the maximum number of entries for the MetadataReader cache. *

    Default is 256 for a local cache, whereas a shared cache is @@ -112,6 +113,10 @@ public int getCacheLimit() { } } + @Override + public MetadataReader getMetadataReader(String className) throws IOException { + return this.delegate.getMetadataReader(className); + } @Override public MetadataReader getMetadataReader(Resource resource) throws IOException { @@ -119,7 +124,7 @@ public MetadataReader getMetadataReader(Resource resource) throws IOException { // No synchronization necessary... MetadataReader metadataReader = this.metadataReaderCache.get(resource); if (metadataReader == null) { - metadataReader = super.getMetadataReader(resource); + metadataReader = this.delegate.getMetadataReader(resource); this.metadataReaderCache.put(resource, metadataReader); } return metadataReader; @@ -128,14 +133,14 @@ else if (this.metadataReaderCache != null) { synchronized (this.metadataReaderCache) { MetadataReader metadataReader = this.metadataReaderCache.get(resource); if (metadataReader == null) { - metadataReader = super.getMetadataReader(resource); + metadataReader = this.delegate.getMetadataReader(resource); this.metadataReaderCache.put(resource, metadataReader); } return metadataReader; } } else { - return super.getMetadataReader(resource); + return this.delegate.getMetadataReader(resource); } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java index 1a2bfe899dfd..e37379a19055 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MergedAnnotationReadingVisitor.java @@ -25,12 +25,13 @@ import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.AnnotationVisitor; import org.springframework.asm.SpringAsmInfo; import org.springframework.asm.Type; import org.springframework.core.annotation.AnnotationFilter; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -43,11 +44,9 @@ */ class MergedAnnotationReadingVisitor extends AnnotationVisitor { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; - @Nullable - private final Object source; + private final @Nullable Object source; private final Class annotationType; @@ -81,8 +80,7 @@ public void visitEnum(String name, String descriptor, String value) { } @Override - @Nullable - public AnnotationVisitor visitAnnotation(String name, String descriptor) { + public @Nullable AnnotationVisitor visitAnnotation(String name, String descriptor) { return visitAnnotation(descriptor, annotation -> this.attributes.put(name, annotation)); } @@ -108,8 +106,7 @@ public > void visitEnum(String descriptor, String value, Consu } @SuppressWarnings("unchecked") - @Nullable - private AnnotationVisitor visitAnnotation( + private @Nullable AnnotationVisitor visitAnnotation( String descriptor, Consumer> consumer) { String className = Type.getType(descriptor).getClassName(); @@ -121,8 +118,7 @@ private AnnotationVisitor visitAnnotation( } @SuppressWarnings("unchecked") - @Nullable - static AnnotationVisitor get(@Nullable ClassLoader classLoader, + static @Nullable AnnotationVisitor get(@Nullable ClassLoader classLoader, @Nullable Object source, String descriptor, boolean visible, Consumer> consumer) { @@ -173,8 +169,7 @@ public void visitEnum(String name, String descriptor, String value) { } @Override - @Nullable - public AnnotationVisitor visitAnnotation(String name, String descriptor) { + public @Nullable AnnotationVisitor visitAnnotation(String name, String descriptor) { return MergedAnnotationReadingVisitor.this.visitAnnotation(descriptor, this.elements::add); } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java index 4eddbfa6cd24..ecae86b6117d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactory.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,13 +18,17 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; /** * Factory interface for {@link MetadataReader} instances. * Allows for caching a MetadataReader per original resource. * * @author Juergen Hoeller + * @author Brian Clozel * @since 2.5 * @see SimpleMetadataReaderFactory * @see CachingMetadataReaderFactory @@ -49,4 +53,23 @@ public interface MetadataReaderFactory { */ MetadataReader getMetadataReader(Resource resource) throws IOException; + /** + * Create a default {@link MetadataReaderFactory} implementation that's suitable + * for the current JVM. + * @return a new factory instance + * @since 7.0 + */ + static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) { + return MetadataReaderFactoryDelegate.create(resourceLoader); + } + + /** + * Create a default {@link MetadataReaderFactory} implementation that's suitable + * for the current JVM. + * @return a new factory instance + * @since 7.0 + */ + static MetadataReaderFactory create(@Nullable ClassLoader classLoader) { + return MetadataReaderFactoryDelegate.create(classLoader); + } } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java new file mode 100644 index 000000000000..dfa317e94b02 --- /dev/null +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.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.type.classreading; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.io.ResourceLoader; + +/** + * Internal delegate for instantiating {@link MetadataReaderFactory} implementations. + * For JDK < 24, the {@link SimpleMetadataReaderFactory} is being used. + * + * @author Brian Clozel + * @since 7.0 + * @see MetadataReaderFactory + */ +abstract class MetadataReaderFactoryDelegate { + + static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) { + return new SimpleMetadataReaderFactory(resourceLoader); + } + + static MetadataReaderFactory create(@Nullable ClassLoader classLoader) { + return new SimpleMetadataReaderFactory(classLoader); + } +} diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java index ba4c84f1c27f..ceb3bb698cd8 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadata.java @@ -20,11 +20,12 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Opcodes; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.MethodMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -42,11 +43,9 @@ final class SimpleAnnotationMetadata implements AnnotationMetadata { private final int access; - @Nullable - private final String enclosingClassName; + private final @Nullable String enclosingClassName; - @Nullable - private final String superClassName; + private final @Nullable String superClassName; private final boolean independentInnerClass; @@ -58,8 +57,7 @@ final class SimpleAnnotationMetadata implements AnnotationMetadata { private final MergedAnnotations mergedAnnotations; - @Nullable - private Set annotationTypes; + private @Nullable Set annotationTypes; SimpleAnnotationMetadata(String className, int access, @Nullable String enclosingClassName, @@ -108,14 +106,12 @@ public boolean isIndependent() { } @Override - @Nullable - public String getEnclosingClassName() { + public @Nullable String getEnclosingClassName() { return this.enclosingClassName; } @Override - @Nullable - public String getSuperClassName() { + public @Nullable String getSuperClassName() { return this.superClassName; } diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java index a4e1128e6244..d3b72faed06e 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleAnnotationMetadataReadingVisitor.java @@ -19,6 +19,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.AnnotationVisitor; import org.springframework.asm.ClassVisitor; import org.springframework.asm.MethodVisitor; @@ -27,7 +29,6 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.MethodMetadata; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -40,18 +41,15 @@ */ final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private String className = ""; private int access; - @Nullable - private String superClassName; + private @Nullable String superClassName; - @Nullable - private String enclosingClassName; + private @Nullable String enclosingClassName; private boolean independentInnerClass; @@ -63,11 +61,9 @@ final class SimpleAnnotationMetadataReadingVisitor extends ClassVisitor { private final Set declaredMethods = new LinkedHashSet<>(4); - @Nullable - private SimpleAnnotationMetadata metadata; + private @Nullable SimpleAnnotationMetadata metadata; - @Nullable - private Source source; + private @Nullable Source source; SimpleAnnotationMetadataReadingVisitor(@Nullable ClassLoader classLoader) { @@ -111,15 +107,13 @@ else if (this.className.equals(outerClassName)) { } @Override - @Nullable - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + public @Nullable AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { return MergedAnnotationReadingVisitor.get(this.classLoader, getSource(), descriptor, visible, this.annotations::add); } @Override - @Nullable - public MethodVisitor visitMethod( + public @Nullable MethodVisitor visitMethod( int access, String name, String descriptor, String signature, String[] exceptions) { // Skip bridge methods and constructors - we're only interested in original user methods. diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java index 08150a51a476..fece634b13a1 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReader.java @@ -19,12 +19,12 @@ import java.io.IOException; import java.io.InputStream; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.ClassReader; import org.springframework.core.io.Resource; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.ClassMetadata; -import org.springframework.lang.Nullable; - /** * {@link MetadataReader} implementation based on an ASM * {@link org.springframework.asm.ClassReader}. diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java index 46aa8f4771d8..58a834a309bd 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMetadataReaderFactory.java @@ -19,10 +19,11 @@ import java.io.FileNotFoundException; import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java index 4b4a0ac5e5fe..c7016ff8540d 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadata.java @@ -16,10 +16,11 @@ package org.springframework.core.type.classreading; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Opcodes; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.type.MethodMetadata; -import org.springframework.lang.Nullable; /** * {@link MethodMetadata} created from a {@link SimpleMethodMetadataReadingVisitor}. diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java index 895887263e53..02a521baf613 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/SimpleMethodMetadataReadingVisitor.java @@ -20,13 +20,15 @@ import java.util.List; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.AnnotationVisitor; import org.springframework.asm.MethodVisitor; +import org.springframework.asm.Opcodes; import org.springframework.asm.SpringAsmInfo; import org.springframework.asm.Type; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.lang.Nullable; /** * ASM method visitor that creates {@link SimpleMethodMetadata}. @@ -38,8 +40,7 @@ */ final class SimpleMethodMetadataReadingVisitor extends MethodVisitor { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final String declaringClassName; @@ -53,8 +54,7 @@ final class SimpleMethodMetadataReadingVisitor extends MethodVisitor { private final Consumer consumer; - @Nullable - private Source source; + private @Nullable Source source; SimpleMethodMetadataReadingVisitor(@Nullable ClassLoader classLoader, String declaringClassName, @@ -71,8 +71,7 @@ final class SimpleMethodMetadataReadingVisitor extends MethodVisitor { @Override - @Nullable - public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + public @Nullable AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { return MergedAnnotationReadingVisitor.get(this.classLoader, getSource(), descriptor, visible, this.annotations::add); } @@ -89,7 +88,7 @@ public void visitEnd() { private Object getSource() { Source source = this.source; if (source == null) { - source = new Source(this.declaringClassName, this.methodName, this.descriptor); + source = new Source(this.declaringClassName, this.methodName, this.access, this.descriptor); this.source = source; } return source; @@ -105,14 +104,16 @@ static final class Source { private final String methodName; + private final int access; + private final String descriptor; - @Nullable - private String toStringValue; + private @Nullable String toStringValue; - Source(String declaringClassName, String methodName, String descriptor) { + Source(String declaringClassName, String methodName, int access, String descriptor) { this.declaringClassName = declaringClassName; this.methodName = methodName; + this.access = access; this.descriptor = descriptor; } @@ -121,6 +122,7 @@ public int hashCode() { int result = 1; result = 31 * result + this.declaringClassName.hashCode(); result = 31 * result + this.methodName.hashCode(); + result = 31 * result + this.access; result = 31 * result + this.descriptor.hashCode(); return result; } @@ -135,7 +137,8 @@ public boolean equals(@Nullable Object other) { } Source otherSource = (Source) other; return (this.declaringClassName.equals(otherSource.declaringClassName) && - this.methodName.equals(otherSource.methodName) && this.descriptor.equals(otherSource.descriptor)); + this.methodName.equals(otherSource.methodName) && + this.access == otherSource.access && this.descriptor.equals(otherSource.descriptor)); } @Override @@ -143,6 +146,27 @@ public String toString() { String value = this.toStringValue; if (value == null) { StringBuilder builder = new StringBuilder(); + if ((this.access & Opcodes.ACC_PUBLIC) != 0) { + builder.append("public "); + } + if ((this.access & Opcodes.ACC_PROTECTED) != 0) { + builder.append("protected "); + } + if ((this.access & Opcodes.ACC_PRIVATE) != 0) { + builder.append("private "); + } + if ((this.access & Opcodes.ACC_ABSTRACT) != 0) { + builder.append("abstract "); + } + if ((this.access & Opcodes.ACC_STATIC) != 0) { + builder.append("static "); + } + if ((this.access & Opcodes.ACC_FINAL) != 0) { + builder.append("final "); + } + Type returnType = Type.getReturnType(this.descriptor); + builder.append(returnType.getClassName()); + builder.append(' '); builder.append(this.declaringClassName); builder.append('.'); builder.append(this.methodName); diff --git a/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java b/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java index 37a94af610b3..2adda8552098 100644 --- a/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/type/classreading/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for reading annotation and class-level metadata. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.type.classreading; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java index ee7d6d5046e2..119ba4fb015b 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AbstractTypeHierarchyTraversingFilter.java @@ -20,11 +20,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.type.ClassMetadata; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.lang.Nullable; /** * Type filter that is aware of traversing over hierarchy. @@ -146,16 +146,14 @@ protected boolean matchClassName(String className) { /** * Override this to match on supertype name. */ - @Nullable - protected Boolean matchSuperClass(String superClassName) { + protected @Nullable Boolean matchSuperClass(String superClassName) { return null; } /** * Override this to match on interface type name. */ - @Nullable - protected Boolean matchInterface(String interfaceName) { + protected @Nullable Boolean matchInterface(String interfaceName) { return null; } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java index 5584edcb688a..114adc96d021 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AnnotationTypeFilter.java @@ -19,10 +19,11 @@ import java.lang.annotation.Annotation; import java.lang.annotation.Inherited; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.type.AnnotationMetadata; import org.springframework.core.type.classreading.MetadataReader; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -102,19 +103,16 @@ protected boolean matchSelf(MetadataReader metadataReader) { } @Override - @Nullable - protected Boolean matchSuperClass(String superClassName) { + protected @Nullable Boolean matchSuperClass(String superClassName) { return hasAnnotation(superClassName); } @Override - @Nullable - protected Boolean matchInterface(String interfaceName) { + protected @Nullable Boolean matchInterface(String interfaceName) { return hasAnnotation(interfaceName); } - @Nullable - protected Boolean hasAnnotation(String typeName) { + protected @Nullable Boolean hasAnnotation(String typeName) { if (Object.class.getName().equals(typeName)) { return false; } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java index 86fbde43636e..c78cb7eb8b15 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AspectJTypeFilter.java @@ -28,10 +28,10 @@ import org.aspectj.weaver.patterns.PatternParser; import org.aspectj.weaver.patterns.SimpleScope; import org.aspectj.weaver.patterns.TypePattern; +import org.jspecify.annotations.Nullable; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.lang.Nullable; /** * Type filter that uses AspectJ type pattern for matching. diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java index 53566f60a739..7293eb297396 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/AssignableTypeFilter.java @@ -16,7 +16,8 @@ package org.springframework.core.type.filter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** @@ -55,19 +56,16 @@ protected boolean matchClassName(String className) { } @Override - @Nullable - protected Boolean matchSuperClass(String superClassName) { + protected @Nullable Boolean matchSuperClass(String superClassName) { return matchTargetType(superClassName); } @Override - @Nullable - protected Boolean matchInterface(String interfaceName) { + protected @Nullable Boolean matchInterface(String interfaceName) { return matchTargetType(interfaceName); } - @Nullable - protected Boolean matchTargetType(String typeName) { + protected @Nullable Boolean matchTargetType(String typeName) { if (this.targetType.getName().equals(typeName)) { return true; } diff --git a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java index dacedaf28a13..6589c371cb62 100644 --- a/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/type/filter/package-info.java @@ -1,9 +1,7 @@ /** * Core support package for type filtering (for example, for classpath scanning). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.type.filter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/core/type/package-info.java b/spring-core/src/main/java/org/springframework/core/type/package-info.java index f63ddb1be36b..6b0d6a987bbb 100644 --- a/spring-core/src/main/java/org/springframework/core/type/package-info.java +++ b/spring-core/src/main/java/org/springframework/core/type/package-info.java @@ -1,9 +1,7 @@ /** * Core support package for type introspection. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.core.type; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 8ec9fb0334a0..8bad805febed 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-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. @@ -42,6 +42,7 @@ * @author Sebastien Deleuze * @author Juergen Hoeller * @since 5.0 + * @deprecated use {@link org.jspecify.annotations.NonNull} instead * @see NonNullApi * @see NonNullFields * @see Nullable @@ -51,5 +52,6 @@ @Documented @Nonnull @TypeQualifierNickname +@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 e2418426a6cf..54cb21ddf18a 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-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. @@ -38,6 +38,7 @@ * @author Sebastien Deleuze * @author Juergen Hoeller * @since 5.0 + * @deprecated use {@link org.jspecify.annotations.NullMarked} instead * @see NonNullFields * @see Nullable * @see NonNull @@ -47,5 +48,6 @@ @Documented @Nonnull @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) +@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 3bbaba3b7ce1..21e3a57dfb8b 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-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,6 +37,7 @@ * * @author Sebastien Deleuze * @since 5.0 + * @deprecated use {@link org.jspecify.annotations.NullMarked} instead * @see NonNullApi * @see Nullable * @see NonNull @@ -46,5 +47,6 @@ @Documented @Nonnull @TypeQualifierDefault(ElementType.FIELD) +@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 a07eec0b7c07..95b8713c0dfa 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,6 +42,7 @@ * @author Sebastien Deleuze * @author Juergen Hoeller * @since 5.0 + * @deprecated use {@link org.jspecify.annotations.Nullable} instead * @see NonNullApi * @see NonNullFields * @see NonNull @@ -51,5 +52,6 @@ @Documented @CheckForNull @TypeQualifierNickname +@Deprecated(since = "7.0") public @interface Nullable { } diff --git a/spring-core/src/main/java/org/springframework/lang/package-info.java b/spring-core/src/main/java/org/springframework/lang/package-info.java index f06fca0539ba..e159afd4883e 100644 --- a/spring-core/src/main/java/org/springframework/lang/package-info.java +++ b/spring-core/src/main/java/org/springframework/lang/package-info.java @@ -7,4 +7,7 @@ * (for example, FindBugs or Animal Sniffer), alternative JVM languages (for example, Kotlin), as well as IDEs * (for example, IntelliJ IDEA or Eclipse with corresponding project setup). */ +@NullMarked package org.springframework.lang; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java index 755ddb7232cb..11b3aa004cae 100644 --- a/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java +++ b/spring-core/src/main/java/org/springframework/objenesis/SpringObjenesis.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -69,7 +69,7 @@ public SpringObjenesis(InstantiatorStrategy strategy) { this.strategy = (strategy != null ? strategy : new StdInstantiatorStrategy()); // Evaluate the "spring.objenesis.ignore" property upfront... - if (SpringProperties.getFlag(SpringObjenesis.IGNORE_OBJENESIS_PROPERTY_NAME)) { + if (SpringProperties.getFlag(IGNORE_OBJENESIS_PROPERTY_NAME)) { this.worthTrying = Boolean.FALSE; } } diff --git a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java index 9afaf2532381..53708948e36b 100644 --- a/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/AntPathMatcher.java @@ -25,7 +25,7 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link PathMatcher} implementation for Ant-style path patterns. @@ -91,8 +91,7 @@ public class AntPathMatcher implements PathMatcher { private boolean trimTokens = false; - @Nullable - private volatile Boolean cachePatterns; + private volatile @Nullable Boolean cachePatterns; private final Map tokenizedPatternCache = new ConcurrentHashMap<>(256); @@ -275,8 +274,8 @@ else if (!fullMatch && "**".equals(pattDirs[pattIdxStart])) { if (!matchStrings(pattDir, pathDirs[pathIdxEnd], uriTemplateVariables)) { return false; } - if (pattIdxEnd == (pattDirs.length - 1) - && pattern.endsWith(this.pathSeparator) != path.endsWith(this.pathSeparator)) { + if (pattIdxEnd == (pattDirs.length - 1) && + pattern.endsWith(this.pathSeparator) != path.endsWith(this.pathSeparator)) { return false; } pattIdxEnd--; @@ -654,8 +653,7 @@ protected static class AntPathStringMatcher { private final boolean exactMatch; - @Nullable - private final Pattern pattern; + private final @Nullable Pattern pattern; private final List variableNames = new ArrayList<>(); @@ -856,8 +854,8 @@ else if (info2.getUriVars() < info1.getUriVars()) { */ private static class PatternInfo { - @Nullable - private final String pattern; + + private final @Nullable String pattern; private int uriVars; @@ -869,8 +867,7 @@ private static class PatternInfo { private boolean prefixPattern; - @Nullable - private Integer length; + private @Nullable Integer length; PatternInfo(@Nullable String pattern, String pathSeparator) { this.pattern = pattern; diff --git a/spring-core/src/main/java/org/springframework/util/Assert.java b/spring-core/src/main/java/org/springframework/util/Assert.java index 71db4555051b..0772c4fea501 100644 --- a/spring-core/src/main/java/org/springframework/util/Assert.java +++ b/spring-core/src/main/java/org/springframework/util/Assert.java @@ -20,8 +20,9 @@ import java.util.Map; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; /** * Assertion utility class that assists in validating arguments. @@ -318,7 +319,7 @@ public static void doesNotContain(@Nullable String textToSearch, String substrin * @throws IllegalArgumentException if the object array is {@code null} or contains no elements */ @Contract("null, _ -> fail") - public static void notEmpty(@Nullable Object[] array, String message) { + public static void notEmpty(@Nullable Object @Nullable [] array, String message) { if (ObjectUtils.isEmpty(array)) { throw new IllegalArgumentException(message); } @@ -337,7 +338,7 @@ public static void notEmpty(@Nullable Object[] array, String message) { * @since 5.0 */ @Contract("null, _ -> fail") - public static void notEmpty(@Nullable Object[] array, Supplier messageSupplier) { + public static void notEmpty(Object @Nullable [] array, Supplier messageSupplier) { if (ObjectUtils.isEmpty(array)) { throw new IllegalArgumentException(nullSafeGet(messageSupplier)); } @@ -351,7 +352,7 @@ public static void notEmpty(@Nullable Object[] array, Supplier messageSu * @param message the exception message to use if the assertion fails * @throws IllegalArgumentException if the object array contains a {@code null} element */ - public static void noNullElements(@Nullable Object[] array, String message) { + public static void noNullElements(Object @Nullable [] array, String message) { if (array != null) { for (Object element : array) { if (element == null) { @@ -373,7 +374,7 @@ public static void noNullElements(@Nullable Object[] array, String message) { * @throws IllegalArgumentException if the object array contains a {@code null} element * @since 5.0 */ - public static void noNullElements(@Nullable Object[] array, Supplier messageSupplier) { + public static void noNullElements(Object @Nullable [] array, Supplier messageSupplier) { if (array != null) { for (Object element : array) { if (element == null) { @@ -644,8 +645,7 @@ private static String messageWithTypeName(String msg, @Nullable Object typeName) return msg + (msg.endsWith(" ") ? "" : ": ") + typeName; } - @Nullable - private static String nullSafeGet(@Nullable Supplier messageSupplier) { + private static @Nullable String nullSafeGet(@Nullable Supplier messageSupplier) { return (messageSupplier != null ? messageSupplier.get() : null); } diff --git a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java index 4a01bd502128..45dcae322533 100644 --- a/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java +++ b/spring-core/src/main/java/org/springframework/util/AutoPopulatingList.java @@ -25,7 +25,7 @@ import java.util.List; import java.util.ListIterator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple {@link List} wrapper class that allows for elements to be 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 91df05d57387..b5a933c1d595 100644 --- a/spring-core/src/main/java/org/springframework/util/ClassUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ClassUtils.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,7 +52,9 @@ import java.util.UUID; import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Miscellaneous {@code java.lang.Class} utility methods. @@ -74,12 +76,6 @@ public abstract class ClassUtils { /** Suffix for array class names: {@code "[]"}. */ public static final String ARRAY_SUFFIX = "[]"; - /** Prefix for internal array class names: {@code "["}. */ - private static final String INTERNAL_ARRAY_PREFIX = "["; - - /** Prefix for internal non-primitive array class names: {@code "[L"}. */ - private static final String NON_PRIMITIVE_ARRAY_PREFIX = "[L"; - /** A reusable empty class array constant. */ private static final Class[] EMPTY_CLASS_ARRAY = {}; @@ -221,8 +217,7 @@ private static void registerCommonClasses(Class... commonClasses) { * @see Thread#getContextClassLoader() * @see ClassLoader#getSystemClassLoader() */ - @Nullable - public static ClassLoader getDefaultClassLoader() { + public static @Nullable ClassLoader getDefaultClassLoader() { ClassLoader cl = null; try { cl = Thread.currentThread().getContextClassLoader(); @@ -253,8 +248,8 @@ public static ClassLoader getDefaultClassLoader() { * @param classLoaderToUse the actual ClassLoader to use for the thread context * @return the original thread context ClassLoader, or {@code null} if not overridden */ - @Nullable - public static ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader classLoaderToUse) { + @Contract("null -> null") + public static @Nullable ClassLoader overrideThreadContextClassLoader(@Nullable ClassLoader classLoaderToUse) { Thread currentThread = Thread.currentThread(); ClassLoader threadContextClassLoader = currentThread.getContextClassLoader(); if (classLoaderToUse != null && !classLoaderToUse.equals(threadContextClassLoader)) { @@ -299,20 +294,6 @@ public static Class forName(String name, @Nullable ClassLoader classLoader) return elementClass.arrayType(); } - // "[Ljava.lang.String;" style arrays - if (name.startsWith(NON_PRIMITIVE_ARRAY_PREFIX) && name.endsWith(";")) { - String elementName = name.substring(NON_PRIMITIVE_ARRAY_PREFIX.length(), name.length() - 1); - Class elementClass = forName(elementName, classLoader); - return elementClass.arrayType(); - } - - // "[[I" or "[[Ljava.lang.String;" style arrays - if (name.startsWith(INTERNAL_ARRAY_PREFIX)) { - String elementName = name.substring(INTERNAL_ARRAY_PREFIX.length()); - Class elementClass = forName(elementName, classLoader); - return elementClass.arrayType(); - } - ClassLoader clToUse = classLoader; if (clToUse == null) { clToUse = getDefaultClassLoader(); @@ -408,6 +389,7 @@ public static boolean isPresent(String className, @Nullable ClassLoader classLoa * @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; @@ -495,8 +477,8 @@ private static boolean isLoadable(Class clazz, ClassLoader classLoader) { * @return the primitive class, or {@code null} if the name does not denote * a primitive class or primitive array class */ - @Nullable - public static Class resolvePrimitiveClassName(@Nullable String name) { + @Contract("null -> null") + public static @Nullable Class resolvePrimitiveClassName(@Nullable String name) { Class result = null; // Most class names will be quite long, considering that they // SHOULD sit in a package, so a length check is worthwhile. @@ -561,7 +543,7 @@ public static boolean isPrimitiveWrapperArray(Class clazz) { * @param clazz the class to check * @return the original class, or a primitive wrapper for the original primitive type */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public static Class resolvePrimitiveIfNecessary(Class clazz) { Assert.notNull(clazz, "Class must not be null"); return (clazz.isPrimitive() && clazz != void.class ? primitiveTypeToWrapperMap.get(clazz) : clazz); @@ -575,6 +557,7 @@ public static Class resolvePrimitiveIfNecessary(Class clazz) { * @see Void * @see Void#TYPE */ + @Contract("null -> false") public static boolean isVoidType(@Nullable Class type) { return (type == void.class || type == Void.class); } @@ -637,10 +620,11 @@ public static boolean isAssignable(Class lhsType, Class rhsType) { 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; } /** @@ -883,8 +867,8 @@ public static Class createCompositeInterface(Class[] interfaces, @Nullable * given classes is {@code null}, the other class will be returned. * @since 3.2.6 */ - @Nullable - public static Class determineCommonAncestor(@Nullable Class clazz1, @Nullable Class clazz2) { + @Contract("null, _ -> param2; _, null -> param1") + public static @Nullable Class determineCommonAncestor(@Nullable Class clazz1, @Nullable Class clazz2) { if (clazz1 == null) { return clazz2; } @@ -962,10 +946,10 @@ public static boolean isLambdaClass(Class clazz) { * 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()); } @@ -974,10 +958,11 @@ public static boolean isCglibProxy(Object object) { * 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())); } @@ -986,10 +971,11 @@ public static boolean isCglibProxyClass(@Nullable Class clazz) { * 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)); } @@ -1030,8 +1016,8 @@ public static Class getUserClass(Class clazz) { * @param value the value to introspect * @return the qualified name of the class */ - @Nullable - public static String getDescriptiveType(@Nullable Object value) { + @Contract("null -> null") + public static @Nullable String getDescriptiveType(@Nullable Object value) { if (value == null) { return null; } @@ -1054,6 +1040,7 @@ public static String getDescriptiveType(@Nullable Object value) { * @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()))); @@ -1194,8 +1181,7 @@ public static boolean hasConstructor(Class clazz, Class... paramTypes) { * @return the constructor, or {@code null} if not found * @see Class#getConstructor */ - @Nullable - public static Constructor getConstructorIfAvailable(Class clazz, Class... paramTypes) { + public static @Nullable Constructor getConstructorIfAvailable(Class clazz, Class... paramTypes) { Assert.notNull(clazz, "Class must not be null"); try { return clazz.getConstructor(paramTypes); @@ -1288,8 +1274,7 @@ else if (candidates.isEmpty()) { * @return the method, or {@code null} if not found * @see Class#getMethod */ - @Nullable - public static Method getMethodIfAvailable(Class clazz, String methodName, @Nullable Class... paramTypes) { + public static @Nullable Method getMethodIfAvailable(Class clazz, String methodName, @Nullable Class @Nullable ... paramTypes) { Assert.notNull(clazz, "Class must not be null"); Assert.notNull(methodName, "Method name must not be null"); if (paramTypes != null) { @@ -1414,7 +1399,7 @@ public static Method getMostSpecificMethod(Method method, @Nullable Class tar * @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); } @@ -1460,8 +1445,7 @@ private static Method getInterfaceMethodIfPossible(Method method, @Nullable Clas return (result != null ? result : method); } - @Nullable - private static Method findInterfaceMethodIfPossible(String methodName, Class[] parameterTypes, + private static @Nullable Method findInterfaceMethodIfPossible(String methodName, Class[] parameterTypes, Class startClass, Class endClass, boolean requirePublicInterface) { Class current = startClass; @@ -1526,8 +1510,7 @@ public static Method getPubliclyAccessibleMethodIfPossible(Method method, @Nulla return (result != null ? result : method); } - @Nullable - private static Method findPubliclyAccessibleMethodIfPossible( + private static @Nullable Method findPubliclyAccessibleMethodIfPossible( String methodName, Class[] parameterTypes, Class declaringClass) { Class current = declaringClass.getSuperclass(); @@ -1589,8 +1572,7 @@ private static boolean isOverridable(Method method, @Nullable Class targetCla * @return the static method, or {@code null} if no static method was found * @throws IllegalArgumentException if the method name is blank or the clazz is null */ - @Nullable - public static Method getStaticMethod(Class clazz, String methodName, Class... args) { + public static @Nullable Method getStaticMethod(Class clazz, String methodName, Class... args) { Assert.notNull(clazz, "Class must not be null"); Assert.notNull(methodName, "Method name must not be null"); try { @@ -1603,8 +1585,7 @@ public static Method getStaticMethod(Class clazz, String methodName, Class } - @Nullable - private static Method getMethodOrNull(Class clazz, String methodName, Class[] paramTypes) { + private static @Nullable Method getMethodOrNull(Class clazz, String methodName, @Nullable Class @Nullable [] paramTypes) { try { return clazz.getMethod(methodName, paramTypes); } 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 65ff2debd911..d1c0c46a7094 100644 --- a/spring-core/src/main/java/org/springframework/util/CollectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/CollectionUtils.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,8 +34,9 @@ import java.util.function.BiFunction; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; /** * Miscellaneous collection utility methods. @@ -63,7 +64,7 @@ public abstract class CollectionUtils { * @return whether the given Collection is empty */ @Contract("null -> true") - public static boolean isEmpty(@Nullable Collection collection) { + public static boolean isEmpty(@Nullable Collection collection) { return (collection == null || collection.isEmpty()); } @@ -74,7 +75,7 @@ public static boolean isEmpty(@Nullable Collection collection) { * @return whether the given Map is empty */ @Contract("null -> true") - public static boolean isEmpty(@Nullable Map map) { + public static boolean isEmpty(@Nullable Map map) { return (map == null || map.isEmpty()); } @@ -102,7 +103,7 @@ public static HashMap newHashMap(int expectedSize) { *

    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 @@ -199,6 +200,7 @@ public static void mergePropertiesIntoMap(@Nullable Properties props, Map * @param element the element to look for * @return {@code true} if found, {@code false} otherwise */ + @Contract("null, _ -> false") public static boolean contains(@Nullable Iterator iterator, Object element) { if (iterator != null) { while (iterator.hasNext()) { @@ -217,6 +219,7 @@ public static boolean contains(@Nullable Iterator iterator, Object element) { * @param element the element to look for * @return {@code true} if found, {@code false} otherwise */ + @Contract("null, _ -> false") public static boolean contains(@Nullable Enumeration enumeration, Object element) { if (enumeration != null) { while (enumeration.hasMoreElements()) { @@ -237,6 +240,7 @@ public static boolean contains(@Nullable Enumeration enumeration, Object elem * @param element the element to look for * @return {@code true} if found, {@code false} otherwise */ + @Contract("null, _ -> false") public static boolean containsInstance(@Nullable Collection collection, Object element) { if (collection != null) { for (Object candidate : collection) { @@ -268,8 +272,7 @@ public static boolean containsAny(Collection source, Collection candidates * @param candidates the candidates to search for * @return the first present object, or {@code null} if not found */ - @Nullable - public static E findFirstMatch(Collection source, Collection candidates) { + public static @Nullable E findFirstMatch(Collection source, Collection candidates) { if (isEmpty(source) || isEmpty(candidates)) { return null; } @@ -289,8 +292,8 @@ public static E findFirstMatch(Collection source, Collection candidate * or {@code null} if none or more than one such value found */ @SuppressWarnings("unchecked") - @Nullable - public static T findValueOfType(Collection collection, @Nullable Class type) { + @Contract("null, _ -> null") + public static @Nullable T findValueOfType(@Nullable Collection collection, @Nullable Class type) { if (isEmpty(collection)) { return null; } @@ -316,8 +319,7 @@ public static T findValueOfType(Collection collection, @Nullable Class * @return a value of one of the given types found if there is a clear match, * or {@code null} if none or more than one such value found */ - @Nullable - public static Object findValueOfType(Collection collection, Class[] types) { + public static @Nullable Object findValueOfType(Collection collection, Class[] types) { if (isEmpty(collection) || ObjectUtils.isEmpty(types)) { return null; } @@ -360,8 +362,7 @@ else if (candidate != elem) { * @return the common element type, or {@code null} if no clear * common type has been found (or the collection was empty) */ - @Nullable - public static Class findCommonElementType(Collection collection) { + public static @Nullable Class findCommonElementType(Collection collection) { if (isEmpty(collection)) { return null; } @@ -389,8 +390,8 @@ else if (candidate != val.getClass()) { * @see LinkedHashMap#keySet() * @see java.util.LinkedHashSet */ - @Nullable - public static T firstElement(@Nullable Set set) { + @Contract("null -> null") + public static @Nullable T firstElement(@Nullable Set set) { if (isEmpty(set)) { return null; } @@ -412,8 +413,8 @@ public static T firstElement(@Nullable Set set) { * @return the first element, or {@code null} if none * @since 5.2.3 */ - @Nullable - public static T firstElement(@Nullable List list) { + @Contract("null -> null") + public static @Nullable T firstElement(@Nullable List list) { if (isEmpty(list)) { return null; } @@ -430,8 +431,8 @@ public static T firstElement(@Nullable List list) { * @see LinkedHashMap#keySet() * @see java.util.LinkedHashSet */ - @Nullable - public static T lastElement(@Nullable Set set) { + @Contract("null -> null") + public static @Nullable T lastElement(@Nullable Set set) { if (isEmpty(set)) { return null; } @@ -454,8 +455,8 @@ public static T lastElement(@Nullable Set set) { * @return the last element, or {@code null} if none * @since 5.0.3 */ - @Nullable - public static T lastElement(@Nullable List list) { + @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/CompositeCollection.java b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java index 09e5864891cf..a2783bb2f3ce 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeCollection.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,6 +20,8 @@ import java.util.Collection; import java.util.Iterator; +import org.jspecify.annotations.Nullable; + /** * Composite collection that combines two other collections. This type is only @@ -83,10 +85,10 @@ public Object[] toArray() { } @Override - @SuppressWarnings("unchecked") - public T[] toArray(T[] a) { + @SuppressWarnings({"unchecked","NullAway"}) // Overridden method does not define nullness + public @Nullable T[] toArray(@Nullable T[] a) { int size = this.size(); - T[] result; + @Nullable T[] result; if (a.length >= size) { result = a; } diff --git a/spring-core/src/main/java/org/springframework/util/CompositeMap.java b/spring-core/src/main/java/org/springframework/util/CompositeMap.java index 6678806df479..8902a3ea085b 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeMap.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeMap.java @@ -23,7 +23,7 @@ import java.util.function.BiFunction; import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Composite map that combines two other maps. @@ -42,11 +42,9 @@ final class CompositeMap implements Map { private final Map second; - @Nullable - private final BiFunction putFunction; + private final @Nullable BiFunction putFunction; - @Nullable - private final Consumer> putAllFunction; + private final @Nullable Consumer> putAllFunction; CompositeMap(Map first, Map second) { @@ -97,8 +95,7 @@ public boolean containsValue(Object value) { } @Override - @Nullable - public V get(Object key) { + public @Nullable V get(Object key) { V firstResult = this.first.get(key); if (firstResult != null) { return firstResult; @@ -109,8 +106,7 @@ public V get(Object key) { } @Override - @Nullable - public V put(K key, V value) { + public @Nullable V put(K key, V value) { if (this.putFunction == null) { throw new UnsupportedOperationException(); } @@ -120,8 +116,7 @@ public V put(K key, V value) { } @Override - @Nullable - public V remove(Object key) { + public @Nullable V remove(Object key) { V firstResult = this.first.remove(key); V secondResult = this.second.remove(key); if (firstResult != null) { diff --git a/spring-core/src/main/java/org/springframework/util/CompositeSet.java b/spring-core/src/main/java/org/springframework/util/CompositeSet.java index 4bc86031403a..fb30d3b32ee5 100644 --- a/spring-core/src/main/java/org/springframework/util/CompositeSet.java +++ b/spring-core/src/main/java/org/springframework/util/CompositeSet.java @@ -18,7 +18,7 @@ import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Composite set that combines two other sets. This type is only exposed through 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 46da8e430ca3..cf54df78e9c6 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 boolean isThrottleActive() { /** * 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 @@ protected void beforeAccess() { "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 @@ protected void beforeAccess() { } } + /** + * 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/ConcurrentLruCache.java b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java index 926bb671ce89..af0b34da9fc6 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentLruCache.java @@ -28,7 +28,7 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple LRU (Least Recently Used) cache, bounded by a specified cache capacity. @@ -223,7 +223,6 @@ public boolean contains(K key) { * @return {@code true} if the key was present before, * {@code false} if there was no matching key */ - @Nullable public boolean remove(K key) { final Node node = this.cache.remove(key); if (node == null) { @@ -486,19 +485,16 @@ public void drainAll() { private static final class Node extends AtomicReference> { final K key; - @Nullable - Node prev; + @Nullable Node prev; - @Nullable - Node next; + @Nullable Node next; Node(K key, CacheEntry cacheEntry) { super(cacheEntry); this.key = key; } - @Nullable - public Node getPrevious() { + public @Nullable Node getPrevious() { return this.prev; } @@ -506,8 +502,7 @@ public void setPrevious(@Nullable Node prev) { this.prev = prev; } - @Nullable - public Node getNext() { + public @Nullable Node getNext() { return this.next; } @@ -523,15 +518,12 @@ V getValue() { private static final class EvictionQueue { - @Nullable - Node first; + @Nullable Node first; - @Nullable - Node last; + @Nullable Node last; - @Nullable - Node poll() { + @Nullable Node poll() { if (this.first == null) { return null; } @@ -557,9 +549,7 @@ void add(Node e) { } private boolean contains(Node e) { - return (e.getPrevious() != null) - || (e.getNext() != null) - || (e == this.first); + return (e.getPrevious() != null) || (e.getNext() != null) || (e == this.first); } private void linkLast(final Node e) { diff --git a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java index 090622e01953..0e2e33eddcb1 100644 --- a/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.java +++ b/spring-core/src/main/java/org/springframework/util/ConcurrentReferenceHashMap.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,7 +34,7 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantLock; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@link ConcurrentHashMap} that uses {@link ReferenceType#SOFT soft} or @@ -99,8 +99,7 @@ public class ConcurrentReferenceHashMap extends AbstractMap implemen /** * Late binding entry set. */ - @Nullable - private volatile Set> entrySet; + private volatile @Nullable Set> entrySet; /** @@ -231,16 +230,14 @@ protected int getHash(@Nullable Object o) { } @Override - @Nullable - public V get(@Nullable Object key) { + public @Nullable V get(@Nullable Object key) { Reference ref = getReference(key, Restructure.WHEN_NECESSARY); Entry entry = (ref != null ? ref.get() : null); return (entry != null ? entry.getValue() : null); } @Override - @Nullable - public V getOrDefault(@Nullable Object key, @Nullable V defaultValue) { + public @Nullable V getOrDefault(@Nullable Object key, @Nullable V defaultValue) { Reference ref = getReference(key, Restructure.WHEN_NECESSARY); Entry entry = (ref != null ? ref.get() : null); return (entry != null ? entry.getValue() : defaultValue); @@ -260,30 +257,25 @@ public boolean containsKey(@Nullable Object key) { * @param restructure types of restructure allowed during this call * @return the reference, or {@code null} if not found */ - @Nullable - protected final Reference getReference(@Nullable Object key, Restructure restructure) { + protected final @Nullable Reference getReference(@Nullable Object key, Restructure restructure) { int hash = getHash(key); return getSegmentForHash(hash).getReference(key, hash, restructure); } @Override - @Nullable - public V put(@Nullable K key, @Nullable V value) { + public @Nullable V put(@Nullable K key, @Nullable V value) { return put(key, value, true); } @Override - @Nullable - public V putIfAbsent(@Nullable K key, @Nullable V value) { + public @Nullable V putIfAbsent(@Nullable K key, @Nullable V value) { return put(key, value, false); } - @Nullable - private V put(@Nullable final K key, @Nullable final V value, final boolean overwriteExisting) { + private @Nullable V put(final @Nullable K key, final @Nullable V value, final boolean overwriteExisting) { return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.RESIZE) { @Override - @Nullable - protected V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { if (entry != null) { V oldValue = entry.getValue(); if (overwriteExisting) { @@ -299,12 +291,10 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry, } @Override - @Nullable - public V remove(@Nullable Object key) { + public @Nullable V remove(@Nullable Object key) { return doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override - @Nullable - protected V execute(@Nullable Reference ref, @Nullable Entry entry) { + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry) { if (entry != null) { if (ref != null) { ref.release(); @@ -317,7 +307,7 @@ protected V execute(@Nullable Reference ref, @Nullable Entry entry) } @Override - public boolean remove(@Nullable Object key, @Nullable final Object value) { + public boolean remove(@Nullable Object key, final @Nullable Object value) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_AFTER, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -334,7 +324,7 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e } @Override - public boolean replace(@Nullable K key, @Nullable final V oldValue, @Nullable final V newValue) { + public boolean replace(@Nullable K key, final @Nullable V oldValue, final @Nullable V newValue) { Boolean result = doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override protected Boolean execute(@Nullable Reference ref, @Nullable Entry entry) { @@ -349,12 +339,10 @@ protected Boolean execute(@Nullable Reference ref, @Nullable Entry e } @Override - @Nullable - public V replace(@Nullable K key, @Nullable final V value) { + public @Nullable V replace(@Nullable K key, final @Nullable V value) { return doTask(key, new Task(TaskOption.RESTRUCTURE_BEFORE, TaskOption.SKIP_IF_EMPTY) { @Override - @Nullable - protected V execute(@Nullable Reference ref, @Nullable Entry entry) { + protected @Nullable V execute(@Nullable Reference ref, @Nullable Entry entry) { if (entry != null) { V oldValue = entry.getValue(); entry.setValue(value); @@ -414,8 +402,7 @@ public Set> entrySet() { return entrySet; } - @Nullable - private T doTask(@Nullable Object key, Task task) { + private @Nullable T doTask(@Nullable Object key, Task task) { int hash = getHash(key); return getSegmentForHash(hash).doTask(hash, key, task); } @@ -469,7 +456,7 @@ protected final class Segment extends ReentrantLock { * Array of references indexed using the low order bits from the hash. * This property should only be set along with {@code resizeThreshold}. */ - private volatile Reference[] references; + private volatile @Nullable Reference[] references; /** * The total number of references contained in this segment. This includes chained @@ -490,8 +477,7 @@ public Segment(int initialSize, int resizeThreshold) { this.resizeThreshold = resizeThreshold; } - @Nullable - public Reference getReference(@Nullable Object key, int hash, Restructure restructure) { + public @Nullable Reference getReference(@Nullable Object key, int hash, Restructure restructure) { if (restructure == Restructure.WHEN_NECESSARY) { restructureIfNecessary(false); } @@ -499,7 +485,7 @@ public Reference getReference(@Nullable Object key, int hash, Restructure return null; } // Use a local copy to protect against other threads writing - Reference[] references = this.references; + @Nullable Reference[] references = this.references; int index = getIndex(hash, references); Reference head = references[index]; return findInChain(head, key, hash); @@ -513,8 +499,7 @@ public Reference getReference(@Nullable Object key, int hash, Restructure * @param task the update operation * @return the result of the operation */ - @Nullable - public T doTask(final int hash, @Nullable final Object key, final Task task) { + public @Nullable T doTask(final int hash, final @Nullable Object key, final Task task) { boolean resize = task.hasOption(TaskOption.RESIZE); if (task.hasOption(TaskOption.RESTRUCTURE_BEFORE)) { restructureIfNecessary(resize); @@ -656,8 +641,7 @@ private void restructure(boolean allowResize, @Nullable Reference ref) { } } - @Nullable - private Reference findInChain(Reference ref, @Nullable Object key, int hash) { + private @Nullable Reference findInChain(@Nullable Reference ref, @Nullable Object key, int hash) { Reference currRef = ref; while (currRef != null) { if (currRef.getHash() == hash) { @@ -679,7 +663,7 @@ private Reference[] createReferenceArray(int size) { return new Reference[size]; } - private int getIndex(int hash, Reference[] references) { + private int getIndex(int hash, @Nullable Reference[] references) { return (hash & (references.length - 1)); } @@ -710,8 +694,7 @@ protected interface Reference { /** * Return the referenced entry, or {@code null} if the entry is no longer available. */ - @Nullable - Entry get(); + @Nullable Entry get(); /** * Return the hash for the reference. @@ -721,8 +704,7 @@ protected interface Reference { /** * Return the next reference in the chain, or {@code null} if none. */ - @Nullable - Reference getNext(); + @Nullable Reference getNext(); /** * Release this entry and ensure that it will be returned from @@ -739,11 +721,9 @@ protected interface Reference { */ protected static final class Entry implements Map.Entry { - @Nullable - private final K key; + private final @Nullable K key; - @Nullable - private volatile V value; + private volatile @Nullable V value; public Entry(@Nullable K key, @Nullable V value) { this.key = key; @@ -751,20 +731,17 @@ public Entry(@Nullable K key, @Nullable V value) { } @Override - @Nullable - public K getKey() { + public @Nullable K getKey() { return this.key; } @Override - @Nullable - public V getValue() { + public @Nullable V getValue() { return this.value; } @Override - @Nullable - public V setValue(@Nullable V value) { + public @Nullable V setValue(@Nullable V value) { V previous = this.value; this.value = value; return previous; @@ -812,8 +789,7 @@ public boolean hasOption(TaskOption option) { * @return the result of the task * @see #execute(Reference, Entry) */ - @Nullable - protected T execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { + protected @Nullable T execute(@Nullable Reference ref, @Nullable Entry entry, @Nullable Entries entries) { return execute(ref, entry); } @@ -824,8 +800,7 @@ protected T execute(@Nullable Reference ref, @Nullable Entry entry, * @return the result of the task * @see #execute(Reference, Entry, Entries) */ - @Nullable - protected T execute(@Nullable Reference ref, @Nullable Entry entry) { + protected @Nullable T execute(@Nullable Reference ref, @Nullable Entry entry) { return null; } } @@ -904,17 +879,13 @@ private class EntryIterator implements Iterator> { private int referenceIndex; - @Nullable - private Reference[] references; + private @Nullable Reference @Nullable [] references; - @Nullable - private Reference reference; + private @Nullable Reference reference; - @Nullable - private Entry next; + private @Nullable Entry next; - @Nullable - private Entry last; + private @Nullable Entry last; public EntryIterator() { moveToNextSegment(); @@ -1020,8 +991,7 @@ public Reference createReference(Entry entry, int hash, @Nullable Re * @return a reference to purge or {@code null} */ @SuppressWarnings("unchecked") - @Nullable - public Reference pollForPurge() { + public @Nullable Reference pollForPurge() { return (Reference) this.queue.poll(); } } @@ -1034,8 +1004,7 @@ private static final class SoftEntryReference extends SoftReference nextReference; + private final @Nullable Reference nextReference; public SoftEntryReference(Entry entry, int hash, @Nullable Reference next, ReferenceQueue> queue) { @@ -1051,8 +1020,7 @@ public int getHash() { } @Override - @Nullable - public Reference getNext() { + public @Nullable Reference getNext() { return this.nextReference; } @@ -1070,8 +1038,7 @@ private static final class WeakEntryReference extends WeakReference nextReference; + private final @Nullable Reference nextReference; public WeakEntryReference(Entry entry, int hash, @Nullable Reference next, ReferenceQueue> queue) { @@ -1087,8 +1054,7 @@ public int getHash() { } @Override - @Nullable - public Reference getNext() { + public @Nullable Reference getNext() { return this.nextReference; } diff --git a/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java b/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java index dd4fc28e7f5d..fbb775c6f6bb 100644 --- a/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java +++ b/spring-core/src/main/java/org/springframework/util/CustomizableThreadCreator.java @@ -19,7 +19,7 @@ import java.io.Serializable; import java.util.concurrent.atomic.AtomicInteger; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple customizable helper class for creating new {@link Thread} instances. @@ -41,8 +41,7 @@ public class CustomizableThreadCreator implements Serializable { private boolean daemon = false; - @Nullable - private ThreadGroup threadGroup; + private @Nullable ThreadGroup threadGroup; private final AtomicInteger threadCount = new AtomicInteger(); @@ -136,8 +135,7 @@ public void setThreadGroup(@Nullable ThreadGroup threadGroup) { * Return the thread group that threads should be created in * (or {@code null} for the default group). */ - @Nullable - public ThreadGroup getThreadGroup() { + public @Nullable ThreadGroup getThreadGroup() { return this.threadGroup; } diff --git a/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java b/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java index e91eda569920..02599754ad89 100644 --- a/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java +++ b/spring-core/src/main/java/org/springframework/util/FastByteArrayOutputStream.java @@ -25,7 +25,7 @@ import java.util.Deque; import java.util.Iterator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A speedy alternative to {@link java.io.ByteArrayOutputStream}. Note that @@ -349,8 +349,7 @@ private static final class FastByteArrayInputStream extends UpdateMessageDigestI private final Iterator buffersIterator; - @Nullable - private byte[] currentBuffer; + private byte @Nullable [] currentBuffer; private int currentBufferLength = 0; 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 f68a89524659..388ab5e34108 100644 --- a/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java +++ b/spring-core/src/main/java/org/springframework/util/FileCopyUtils.java @@ -27,7 +27,7 @@ import java.io.Writer; import java.nio.file.Files; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple utility methods for file and stream copying. All copy methods use a block size 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 d060c1c1187e..2bbe365c8d31 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. @@ -26,7 +26,9 @@ import java.nio.file.attribute.BasicFileAttributes; import java.util.EnumSet; -import org.springframework.lang.Nullable; +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 static boolean deleteRecursively(@Nullable File root) { * @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/FilteredIterator.java b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java index ab2c59a592a6..320572152213 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredIterator.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredIterator.java @@ -20,7 +20,7 @@ import java.util.NoSuchElementException; import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link Iterator} that filters out values that do not match a predicate. @@ -37,8 +37,7 @@ final class FilteredIterator implements Iterator { private final Predicate filter; - @Nullable - private E next; + private @Nullable E next; private boolean hasNext; diff --git a/spring-core/src/main/java/org/springframework/util/FilteredMap.java b/spring-core/src/main/java/org/springframework/util/FilteredMap.java index e4bb17d86225..d577fd4e2148 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredMap.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredMap.java @@ -21,7 +21,7 @@ import java.util.Set; import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Map that filters out values that do not match a predicate. @@ -76,8 +76,7 @@ public boolean containsKey(Object key) { @Override @SuppressWarnings("unchecked") - @Nullable - public V get(Object key) { + public @Nullable V get(Object key) { V value = this.delegate.get(key); if (value != null && this.filter.test((K) key)) { return value; @@ -88,8 +87,7 @@ public V get(Object key) { } @Override - @Nullable - public V put(K key, V value) { + public @Nullable V put(K key, V value) { V oldValue = this.delegate.put(key, value); if (oldValue != null && this.filter.test(key)) { return oldValue; @@ -101,8 +99,7 @@ public V put(K key, V value) { @Override @SuppressWarnings("unchecked") - @Nullable - public V remove(Object key) { + public @Nullable V remove(Object key) { V oldValue = this.delegate.remove(key); if (oldValue != null && this.filter.test((K) key)) { return oldValue; diff --git a/spring-core/src/main/java/org/springframework/util/FilteredSet.java b/spring-core/src/main/java/org/springframework/util/FilteredSet.java index 21d6ac6e17e0..acf03d0c3b89 100644 --- a/spring-core/src/main/java/org/springframework/util/FilteredSet.java +++ b/spring-core/src/main/java/org/springframework/util/FilteredSet.java @@ -19,7 +19,7 @@ import java.util.Set; import java.util.function.Predicate; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Set that filters out values that do not match a predicate. diff --git a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java index 08e2000f1a3d..7e2005a28669 100644 --- a/spring-core/src/main/java/org/springframework/util/InstanceFilter.java +++ b/spring-core/src/main/java/org/springframework/util/InstanceFilter.java @@ -19,7 +19,7 @@ import java.util.Collection; import java.util.Collections; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A simple instance filter that checks if a given instance match based on diff --git a/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java b/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java index 57783de953c3..9762e04f508f 100644 --- a/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java +++ b/spring-core/src/main/java/org/springframework/util/InvalidMimeTypeException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2017 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,12 +16,15 @@ package org.springframework.util; +import org.jspecify.annotations.Nullable; + /** * Exception thrown from {@link MimeTypeUtils#parseMimeType(String)} in case of * encountering an invalid content type specification String. * * @author Juergen Hoeller * @author Rossen Stoyanchev + * @author Sebastien Deleuze * @since 4.0 */ @SuppressWarnings("serial") @@ -35,8 +38,10 @@ public class InvalidMimeTypeException extends IllegalArgumentException { * @param mimeType the offending media type * @param message a detail message indicating the invalid part */ - public InvalidMimeTypeException(String mimeType, String message) { - super("Invalid mime type \"" + mimeType + "\": " + message); + public InvalidMimeTypeException(String mimeType, @Nullable String message) { + super(message == null ? + "Invalid mime type \"" + mimeType + "\"" : + "Invalid mime type \"" + mimeType + "\": " + message); this.mimeType = mimeType; } diff --git a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java index 3a1f9f102946..6fbc5fa1bebe 100644 --- a/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java +++ b/spring-core/src/main/java/org/springframework/util/LinkedCaseInsensitiveMap.java @@ -31,7 +31,7 @@ import java.util.function.Consumer; import java.util.function.Function; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link LinkedHashMap} variant that stores String keys in a case-insensitive @@ -56,14 +56,11 @@ public class LinkedCaseInsensitiveMap implements Map, Serializable private final Locale locale; - @Nullable - private transient volatile Set keySet; + private transient volatile @Nullable Set keySet; - @Nullable - private transient volatile Collection values; + private transient volatile @Nullable Collection values; - @Nullable - private transient volatile Set> entrySet; + private transient volatile @Nullable Set> entrySet; /** @@ -164,8 +161,7 @@ public boolean containsValue(Object value) { } @Override - @Nullable - public V get(Object key) { + public @Nullable V get(Object key) { if (key instanceof String string) { String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey(string)); if (caseInsensitiveKey != null) { @@ -176,8 +172,7 @@ public V get(Object key) { } @Override - @Nullable - public V getOrDefault(Object key, V defaultValue) { + public @Nullable V getOrDefault(Object key, V defaultValue) { if (key instanceof String string) { String caseInsensitiveKey = this.caseInsensitiveKeys.get(convertKey(string)); if (caseInsensitiveKey != null) { @@ -188,8 +183,7 @@ public V getOrDefault(Object key, V defaultValue) { } @Override - @Nullable - public V put(String key, @Nullable V value) { + public @Nullable V put(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.put(convertKey(key), key); V oldKeyValue = null; if (oldKey != null && !oldKey.equals(key)) { @@ -208,8 +202,7 @@ public void putAll(Map map) { } @Override - @Nullable - public V putIfAbsent(String key, @Nullable V value) { + public @Nullable V putIfAbsent(String key, @Nullable V value) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { V oldKeyValue = this.targetMap.get(oldKey); @@ -224,8 +217,7 @@ public V putIfAbsent(String key, @Nullable V value) { } @Override - @Nullable - public V computeIfAbsent(String key, Function mappingFunction) { + public @Nullable V computeIfAbsent(String key, Function mappingFunction) { String oldKey = this.caseInsensitiveKeys.putIfAbsent(convertKey(key), key); if (oldKey != null) { V oldKeyValue = this.targetMap.get(oldKey); @@ -240,8 +232,7 @@ public V computeIfAbsent(String key, Function mappi } @Override - @Nullable - public V remove(Object key) { + public @Nullable V remove(Object key) { if (key instanceof String string) { String caseInsensitiveKey = removeCaseInsensitiveKey(string); if (caseInsensitiveKey != null) { @@ -348,8 +339,7 @@ protected boolean removeEldestEntry(Map.Entry eldest) { return false; } - @Nullable - private String removeCaseInsensitiveKey(String key) { + private @Nullable String removeCaseInsensitiveKey(String key) { return this.caseInsensitiveKeys.remove(convertKey(key)); } @@ -494,8 +484,7 @@ private abstract class EntryIterator implements Iterator { private final Iterator> delegate; - @Nullable - private Entry last; + private @Nullable Entry last; public EntryIterator() { this.delegate = targetMap.entrySet().iterator(); 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 8faf71ea1ce0..d383a1c9227f 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 @@ * @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/MethodInvoker.java b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java index 73171aec1de7..eefcafea8c5a 100644 --- a/spring-core/src/main/java/org/springframework/util/MethodInvoker.java +++ b/spring-core/src/main/java/org/springframework/util/MethodInvoker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Helper class that allows for specifying a method to invoke in a declarative @@ -41,24 +41,18 @@ public class MethodInvoker { private static final Object[] EMPTY_ARGUMENTS = new Object[0]; - @Nullable - protected Class targetClass; + protected @Nullable Class targetClass; - @Nullable - private Object targetObject; + private @Nullable Object targetObject; - @Nullable - private String targetMethod; + private @Nullable String targetMethod; - @Nullable - private String staticMethod; + private @Nullable String staticMethod; - @Nullable - private Object[] arguments; + private @Nullable Object @Nullable [] arguments; /** The method we will call. */ - @Nullable - private Method methodObject; + private @Nullable Method methodObject; /** @@ -75,8 +69,7 @@ public void setTargetClass(@Nullable Class targetClass) { /** * Return the target class on which to call the target method. */ - @Nullable - public Class getTargetClass() { + public @Nullable Class getTargetClass() { return this.targetClass; } @@ -97,8 +90,7 @@ public void setTargetObject(@Nullable Object targetObject) { /** * Return the target object on which to call the target method. */ - @Nullable - public Object getTargetObject() { + public @Nullable Object getTargetObject() { return this.targetObject; } @@ -116,8 +108,7 @@ public void setTargetMethod(@Nullable String targetMethod) { /** * Return the name of the method to be invoked. */ - @Nullable - public String getTargetMethod() { + public @Nullable String getTargetMethod() { return this.targetMethod; } @@ -143,7 +134,7 @@ public void setArguments(@Nullable Object... arguments) { /** * Return the arguments for the method invocation. */ - public Object[] getArguments() { + public @Nullable Object[] getArguments() { return (this.arguments != null ? this.arguments : EMPTY_ARGUMENTS); } @@ -175,7 +166,7 @@ public void prepare() throws ClassNotFoundException, NoSuchMethodException { Assert.notNull(targetClass, "Either 'targetClass' or 'targetObject' is required"); Assert.notNull(targetMethod, "Property 'targetMethod' is required"); - Object[] arguments = getArguments(); + @Nullable Object[] arguments = getArguments(); Class[] argTypes = new Class[arguments.length]; for (int i = 0; i < arguments.length; ++i) { argTypes[i] = (arguments[i] != null ? arguments[i].getClass() : Object.class); @@ -213,10 +204,9 @@ protected Class resolveClassName(String className) throws ClassNotFoundExcept * @see #getTargetMethod() * @see #getArguments() */ - @Nullable - protected Method findMatchingMethod() { + protected @Nullable Method findMatchingMethod() { String targetMethod = getTargetMethod(); - Object[] arguments = getArguments(); + @Nullable Object[] arguments = getArguments(); int argCount = arguments.length; Class targetClass = getTargetClass(); @@ -271,8 +261,7 @@ public boolean isPrepared() { * @throws IllegalAccessException if the target method couldn't be accessed * @see #prepare */ - @Nullable - public Object invoke() throws InvocationTargetException, IllegalAccessException { + public @Nullable Object invoke() throws InvocationTargetException, IllegalAccessException { // In the static case, target will simply be {@code null}. Object targetObject = getTargetObject(); Method preparedMethod = getPreparedMethod(); @@ -304,7 +293,7 @@ public Object invoke() throws InvocationTargetException, IllegalAccessException * @param args the arguments to match * @return the accumulated weight for all arguments */ - public static int getTypeDifferenceWeight(Class[] paramTypes, Object[] args) { + public static int getTypeDifferenceWeight(Class[] paramTypes, @Nullable Object[] args) { int result = 0; for (int i = 0; i < paramTypes.length; i++) { if (!ClassUtils.isAssignableValue(paramTypes[i], args[i])) { diff --git a/spring-core/src/main/java/org/springframework/util/MimeType.java b/spring-core/src/main/java/org/springframework/util/MimeType.java index d4b670718c7f..20d51a5b0a47 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeType.java +++ b/spring-core/src/main/java/org/springframework/util/MimeType.java @@ -23,14 +23,13 @@ import java.util.BitSet; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.Locale; import java.util.Map; import java.util.TreeSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents a MIME Type, as originally defined in RFC 2046 and subsequently @@ -102,13 +101,12 @@ public class MimeType implements Comparable, Serializable { private final String subtype; + @SuppressWarnings("serial") private final Map parameters; - @Nullable - private transient Charset resolvedCharset; + private transient @Nullable Charset resolvedCharset; - @Nullable - private volatile String toStringValue; + private volatile @Nullable String toStringValue; /** @@ -296,8 +294,7 @@ public String getSubtype() { * Return the subtype suffix as defined in RFC 6839. * @since 5.3 */ - @Nullable - public String getSubtypeSuffix() { + public @Nullable String getSubtypeSuffix() { int suffixIndex = this.subtype.lastIndexOf('+'); if (suffixIndex != -1 && this.subtype.length() > suffixIndex) { return this.subtype.substring(suffixIndex + 1); @@ -310,8 +307,7 @@ public String getSubtypeSuffix() { * @return the character set, or {@code null} if not available * @since 4.3 */ - @Nullable - public Charset getCharset() { + public @Nullable Charset getCharset() { return this.resolvedCharset; } @@ -320,8 +316,7 @@ public Charset getCharset() { * @param name the parameter name * @return the parameter value, or {@code null} if not present */ - @Nullable - public String getParameter(String name) { + public @Nullable String getParameter(String name) { return this.parameters.get(name); } @@ -695,48 +690,4 @@ private static Map addCharsetParameter(Charset charset, Map the type of mime types that may be compared by this comparator - * @deprecated As of 6.0, with no direct replacement - */ - @Deprecated(since = "6.0", forRemoval = true) - public static class SpecificityComparator implements Comparator { - - @Override - public int compare(T mimeType1, T mimeType2) { - if (mimeType1.isWildcardType() && !mimeType2.isWildcardType()) { // */* < audio/* - return 1; - } - else if (mimeType2.isWildcardType() && !mimeType1.isWildcardType()) { // audio/* > */* - return -1; - } - else if (!mimeType1.getType().equals(mimeType2.getType())) { // audio/basic == text/html - return 0; - } - else { // mediaType1.getType().equals(mediaType2.getType()) - if (mimeType1.isWildcardSubtype() && !mimeType2.isWildcardSubtype()) { // audio/* < audio/basic - return 1; - } - else if (mimeType2.isWildcardSubtype() && !mimeType1.isWildcardSubtype()) { // audio/basic > audio/* - return -1; - } - else if (!mimeType1.getSubtype().equals(mimeType2.getSubtype())) { // audio/basic == audio/wave - return 0; - } - else { // mediaType2.getSubtype().equals(mediaType2.getSubtype()) - return compareParameters(mimeType1, mimeType2); - } - } - } - - protected int compareParameters(T mimeType1, T mimeType2) { - int paramsSize1 = mimeType1.getParameters().size(); - int paramsSize2 = mimeType2.getParameters().size(); - return Integer.compare(paramsSize2, paramsSize1); // audio/basic;level=1 < audio/basic - } - } - } diff --git a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java index 93c946ec6760..0139e1e5c181 100644 --- a/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java +++ b/spring-core/src/main/java/org/springframework/util/MimeTypeUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,7 +22,6 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; -import java.util.Comparator; import java.util.Iterator; import java.util.LinkedHashMap; import java.util.List; @@ -31,7 +30,7 @@ import java.util.function.BiPredicate; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Miscellaneous {@link MimeType} utility methods. @@ -51,14 +50,6 @@ public abstract class MimeTypeUtils { 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'}; - /** - * Comparator formally used by {@link #sortBySpecificity(List)}. - * @deprecated As of 6.0, with no direct replacement - */ - @SuppressWarnings("removal") - @Deprecated(since = "6.0", forRemoval = true) - public static final Comparator SPECIFICITY_COMPARATOR = new MimeType.SpecificityComparator<>(); - /** * Public constant mime type that includes all media ranges (i.e. "*/*"). */ @@ -176,8 +167,7 @@ public abstract class MimeTypeUtils { private static final ConcurrentLruCache cachedMimeTypes = new ConcurrentLruCache<>(64, MimeTypeUtils::parseMimeTypeInternal); - @Nullable - private static volatile Random random; + private static volatile @Nullable Random random; static { // Not using "parseMimeType" to avoid static init cost @@ -213,7 +203,6 @@ public static MimeType parseMimeType(String mimeType) { return cachedMimeTypes.get(mimeType); } - @SuppressWarnings("NullAway") private static MimeType parseMimeTypeInternal(String mimeType) { int index = mimeType.indexOf(';'); String fullType = (index >= 0 ? mimeType.substring(0, index) : mimeType).trim(); diff --git a/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java index 959a69b843ed..e7e80a8643d1 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/MultiToSingleValueMapAdapter.java @@ -28,7 +28,7 @@ import java.util.Set; import java.util.function.BiConsumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Adapts a given {@link MultiValueMap} to the {@link Map} contract. The @@ -46,11 +46,9 @@ final class MultiToSingleValueMapAdapter implements Map, Serializabl private final MultiValueMap targetMap; - @Nullable - private transient Collection values; + private transient @Nullable Collection values; - @Nullable - private transient Set> entries; + private transient @Nullable Set> entries; /** @@ -101,20 +99,17 @@ public boolean containsValue(@Nullable Object value) { } @Override - @Nullable - public V get(Object key) { + public @Nullable V get(Object key) { return adaptValue(this.targetMap.get(key)); } - @Nullable @Override - public V put(K key, @Nullable V value) { + public @Nullable V put(K key, @Nullable V value) { return adaptValue(this.targetMap.put(key, adaptValue(value))); } @Override - @Nullable - public V remove(Object key) { + public @Nullable V remove(Object key) { return adaptValue(this.targetMap.remove(key)); } @@ -205,8 +200,7 @@ public void forEach(BiConsumer action) { this.targetMap.forEach((k, vs) -> action.accept(k, vs.get(0))); } - @Nullable - private V adaptValue(@Nullable List values) { + private @Nullable V adaptValue(@Nullable List values) { if (!CollectionUtils.isEmpty(values)) { return values.get(0); } @@ -215,8 +209,7 @@ private V adaptValue(@Nullable List values) { } } - @Nullable - private List adaptValue(@Nullable V value) { + private @Nullable List adaptValue(@Nullable V value) { if (value != null) { return Collections.singletonList(value); } diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMap.java b/spring-core/src/main/java/org/springframework/util/MultiValueMap.java index 262601a5a4ae..fc6cd12fbf32 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMap.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,7 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Extension of the {@code Map} interface that stores multiple values. @@ -29,15 +29,14 @@ * @param the key type * @param the value element type */ -public interface MultiValueMap extends Map> { +public interface MultiValueMap extends Map> { /** * Return the first value for the given key. * @param key the key * @return the first value for the specified key, or {@code null} if none */ - @Nullable - V getFirst(K key); + @Nullable V getFirst(K key); /** * Add the given single value to the current list of values for the given key. diff --git a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java index 4c7c2f1d133c..97207b8f007b 100644 --- a/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/MultiValueMapAdapter.java @@ -24,7 +24,7 @@ import java.util.Set; import java.util.function.BiConsumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Adapts a given {@link Map} to the {@link MultiValueMap} contract. @@ -56,8 +56,7 @@ public MultiValueMapAdapter(Map> targetMap) { // MultiValueMap implementation @Override - @Nullable - public V getFirst(K key) { + public @Nullable V getFirst(K key) { List values = this.targetMap.get(key); return (!CollectionUtils.isEmpty(values) ? values.get(0) : null); } @@ -126,26 +125,22 @@ public boolean containsValue(Object value) { } @Override - @Nullable - public List get(Object key) { + public @Nullable List get(Object key) { return this.targetMap.get(key); } @Override - @Nullable - public List put(K key, List value) { + public @Nullable List put(K key, List value) { return this.targetMap.put(key, value); } @Override - @Nullable - public List putIfAbsent(K key, List value) { + public @Nullable List putIfAbsent(K key, List value) { return this.targetMap.putIfAbsent(key, value); } @Override - @Nullable - public List remove(Object key) { + public @Nullable List remove(Object key) { return this.targetMap.remove(key); } diff --git a/spring-core/src/main/java/org/springframework/util/NumberUtils.java b/spring-core/src/main/java/org/springframework/util/NumberUtils.java index 27bb5db53b90..6cac75f70cd6 100644 --- a/spring-core/src/main/java/org/springframework/util/NumberUtils.java +++ b/spring-core/src/main/java/org/springframework/util/NumberUtils.java @@ -23,7 +23,7 @@ import java.text.ParseException; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Miscellaneous utility methods for number conversion and parsing. @@ -236,7 +236,7 @@ else if (BigDecimal.class == targetClass || Number.class == targetClass) { * @see #convertNumberToTargetClass * @see #parseNumber(String, Class) */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public static T parseNumber( String text, Class targetClass, @Nullable NumberFormat numberFormat) { 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 07e338a7da4d..1496a2e83867 100644 --- a/spring-core/src/main/java/org/springframework/util/ObjectUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ObjectUtils.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,8 +27,9 @@ import java.util.StringJoiner; import java.util.TimeZone; +import org.jspecify.annotations.Nullable; + import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; /** * Miscellaneous object utility methods. @@ -82,7 +83,7 @@ public static boolean isCheckedException(Throwable ex) { * @param declaredExceptions the exception types declared in the throws clause * @return whether the given exception is compatible */ - public static boolean isCompatibleWithThrowsClause(Throwable ex, @Nullable Class... declaredExceptions) { + public static boolean isCompatibleWithThrowsClause(Throwable ex, Class @Nullable ... declaredExceptions) { if (!isCheckedException(ex)) { return true; } @@ -113,7 +114,7 @@ public static boolean isArray(@Nullable Object obj) { * @see #isEmpty(Object) */ @Contract("null -> true") - public static boolean isEmpty(@Nullable Object[] array) { + public static boolean isEmpty(@Nullable Object @Nullable [] array) { return (array == null || array.length == 0); } @@ -171,8 +172,8 @@ public static boolean isEmpty(@Nullable Object obj) { * if the {@code Optional} is empty, or simply the given object as-is * @since 5.0 */ - @Nullable - public static Object unwrapOptional(@Nullable Object obj) { + @Contract("null -> null") + public static @Nullable Object unwrapOptional(@Nullable Object obj) { if (obj instanceof Optional optional) { Object result = optional.orElse(null); Assert.isTrue(!(result instanceof Optional), "Multi-level Optional usage not supported"); @@ -188,7 +189,8 @@ public static Object unwrapOptional(@Nullable Object obj) { * @param element the element to check for * @return whether the element has been found in the given array */ - public static boolean containsElement(@Nullable Object[] array, Object element) { + @Contract("null, _ -> false") + public static boolean containsElement(@Nullable Object @Nullable [] array, @Nullable Object element) { if (array == null) { return false; } @@ -253,7 +255,7 @@ public static > E caseInsensitiveValueOf(E[] enumValues, Strin * @param obj the object to append * @return the new array (of the same component type; never {@code null}) */ - public static A[] addObjectToArray(@Nullable A[] array, @Nullable O obj) { + public static A[] addObjectToArray(A @Nullable [] array, @Nullable O obj) { return addObjectToArray(array, obj, (array != null ? array.length : 0)); } @@ -266,7 +268,7 @@ public static A[] addObjectToArray(@Nullable A[] array, @Nullab * @return the new array (of the same component type; never {@code null}) * @since 6.0 */ - public static A[] addObjectToArray(@Nullable A[] array, @Nullable O obj, int position) { + public static @Nullable A[] addObjectToArray(A @Nullable [] array, @Nullable O obj, int position) { Class componentType = Object.class; if (array != null) { componentType = array.getClass().componentType(); @@ -276,7 +278,7 @@ else if (obj != null) { } int newArrayLength = (array != null ? array.length + 1 : 1); @SuppressWarnings("unchecked") - A[] newArray = (A[]) Array.newInstance(componentType, newArrayLength); + @Nullable A[] newArray = (A[]) Array.newInstance(componentType, newArrayLength); if (array != null) { System.arraycopy(array, 0, newArray, 0, position); System.arraycopy(array, position, newArray, position + 1, array.length - position); @@ -398,7 +400,7 @@ private static boolean arrayEquals(Object o1, Object o2) { * @return a hash value of the elements * @since 6.1 */ - public static int nullSafeHash(@Nullable Object... elements) { + public static int nullSafeHash(@Nullable Object @Nullable ... elements) { if (elements == null) { return 0; } @@ -410,10 +412,10 @@ public static int nullSafeHash(@Nullable Object... elements) { } /** - * 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 */ @@ -459,7 +461,7 @@ public static int nullSafeHashCode(@Nullable Object obj) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(Object[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable Object[] array) { + public static int nullSafeHashCode(Object @Nullable [] array) { return Arrays.hashCode(array); } @@ -469,7 +471,7 @@ public static int nullSafeHashCode(@Nullable Object[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(boolean[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable boolean[] array) { + public static int nullSafeHashCode(boolean @Nullable [] array) { return Arrays.hashCode(array); } @@ -479,7 +481,7 @@ public static int nullSafeHashCode(@Nullable boolean[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(byte[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable byte[] array) { + public static int nullSafeHashCode(byte @Nullable [] array) { return Arrays.hashCode(array); } @@ -489,7 +491,7 @@ public static int nullSafeHashCode(@Nullable byte[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(char[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable char[] array) { + public static int nullSafeHashCode(char @Nullable [] array) { return Arrays.hashCode(array); } @@ -499,7 +501,7 @@ public static int nullSafeHashCode(@Nullable char[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(double[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable double[] array) { + public static int nullSafeHashCode(double @Nullable [] array) { return Arrays.hashCode(array); } @@ -509,7 +511,7 @@ public static int nullSafeHashCode(@Nullable double[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(float[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable float[] array) { + public static int nullSafeHashCode(float @Nullable [] array) { return Arrays.hashCode(array); } @@ -519,7 +521,7 @@ public static int nullSafeHashCode(@Nullable float[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(int[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable int[] array) { + public static int nullSafeHashCode(int @Nullable [] array) { return Arrays.hashCode(array); } @@ -529,7 +531,7 @@ public static int nullSafeHashCode(@Nullable int[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(long[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable long[] array) { + public static int nullSafeHashCode(long @Nullable [] array) { return Arrays.hashCode(array); } @@ -539,7 +541,7 @@ public static int nullSafeHashCode(@Nullable long[] array) { * @deprecated as of 6.1 in favor of {@link Arrays#hashCode(short[])} */ @Deprecated(since = "6.1") - public static int nullSafeHashCode(@Nullable short[] array) { + public static int nullSafeHashCode(short @Nullable [] array) { return Arrays.hashCode(array); } @@ -651,7 +653,7 @@ public static String nullSafeToString(@Nullable Object obj) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable Object[] array) { + public static String nullSafeToString(@Nullable Object @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -675,7 +677,7 @@ public static String nullSafeToString(@Nullable Object[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable boolean[] array) { + public static String nullSafeToString(boolean @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -699,7 +701,7 @@ public static String nullSafeToString(@Nullable boolean[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable byte[] array) { + public static String nullSafeToString(byte @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -722,7 +724,7 @@ public static String nullSafeToString(@Nullable byte[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable char[] array) { + public static String nullSafeToString(char @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -745,7 +747,7 @@ public static String nullSafeToString(@Nullable char[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable double[] array) { + public static String nullSafeToString(double @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -768,7 +770,7 @@ public static String nullSafeToString(@Nullable double[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable float[] array) { + public static String nullSafeToString(float @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -791,7 +793,7 @@ public static String nullSafeToString(@Nullable float[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable int[] array) { + public static String nullSafeToString(int @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -814,7 +816,7 @@ public static String nullSafeToString(@Nullable int[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable long[] array) { + public static String nullSafeToString(long @Nullable [] array) { if (array == null) { return NULL_STRING; } @@ -838,7 +840,7 @@ public static String nullSafeToString(@Nullable long[] array) { * @param array the array to build a String representation for * @return a String representation of {@code array} */ - public static String nullSafeToString(@Nullable short[] array) { + public static String nullSafeToString(short @Nullable [] array) { if (array == null) { return NULL_STRING; } diff --git a/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java b/spring-core/src/main/java/org/springframework/util/PatternMatchUtils.java index 9f050351f0b6..b4c0d56902f9 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. @@ -16,7 +16,9 @@ package org.springframework.util; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Utility methods for simple pattern matching, in particular for Spring's typical @@ -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 static boolean simpleMatch(@Nullable String pattern, @Nullable String str } int nextIndex = pattern.indexOf('*', 1); if (nextIndex == -1) { - return str.endsWith(pattern.substring(1)); + String part = pattern.substring(1); + return (ignoreCase ? StringUtils.endsWithIgnoreCase(str, part) : str.endsWith(part)); } String part = pattern.substring(1, nextIndex); if (part.isEmpty()) { - return simpleMatch(pattern.substring(nextIndex), str); + return simpleMatch(pattern.substring(nextIndex), str, ignoreCase); } - int partIndex = str.indexOf(part); + int partIndex = indexOf(str, part, 0, ignoreCase); while (partIndex != -1) { - if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()))) { + if (simpleMatch(pattern.substring(nextIndex), str.substring(partIndex + part.length()), ignoreCase)) { return true; } - partIndex = str.indexOf(part, partIndex + 1); + partIndex = indexOf(str, part, partIndex + 1, ignoreCase); } return false; } return (str.length() >= firstIndex && - pattern.startsWith(str.substring(0, firstIndex)) && - simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex))); + checkStartsWith(pattern, str, firstIndex, ignoreCase) && + simpleMatch(pattern.substring(firstIndex), str.substring(firstIndex), ignoreCase)); + } + + private static boolean checkStartsWith(String pattern, String str, int index, boolean ignoreCase) { + String part = str.substring(0, index); + return (ignoreCase ? StringUtils.startsWithIgnoreCase(pattern, part) : pattern.startsWith(part)); + } + + private static int indexOf(String str, String otherStr, int startIndex, boolean ignoreCase) { + if (!ignoreCase) { + return str.indexOf(otherStr, startIndex); + } + for (int i = startIndex; i <= (str.length() - otherStr.length()); i++) { + if (str.regionMatches(true, i, otherStr, 0, otherStr.length())) { + return i; + } + } + return -1; } /** @@ -83,7 +117,8 @@ public static boolean simpleMatch(@Nullable String pattern, @Nullable String str * @param str the String to match * @return whether the String matches any of the given patterns */ - public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String str) { + @Contract("null, _ -> false; _, null -> false") + public static boolean simpleMatch(String @Nullable [] patterns, @Nullable String str) { if (patterns != null) { for (String pattern : patterns) { if (simpleMatch(pattern, str)) { @@ -94,4 +129,20 @@ public static boolean simpleMatch(@Nullable String[] patterns, @Nullable String return false; } + /** + * Variant of {@link #simpleMatch(String[], String)} that ignores upper/lower case. + * @since 6.1.20 + */ + @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/PlaceholderParser.java b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java index 5b538cdcb8a7..651c9b071c44 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderParser.java @@ -26,8 +26,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver; /** @@ -76,13 +76,11 @@ final class PlaceholderParser { private final String simplePrefix; - @Nullable - private final String separator; + private final @Nullable String separator; private final boolean ignoreUnresolvablePlaceholders; - @Nullable - private final Character escape; + private final @Nullable Character escape; /** @@ -316,8 +314,7 @@ static class PartResolutionContext implements PlaceholderResolver { private final PlaceholderResolver resolver; - @Nullable - private Set visitedPlaceholders; + private @Nullable Set visitedPlaceholders; PartResolutionContext(PlaceholderResolver resolver, String prefix, String suffix, @@ -330,8 +327,7 @@ static class PartResolutionContext implements PlaceholderResolver { } @Override - @Nullable - public String resolvePlaceholder(String placeholderName) { + public @Nullable String resolvePlaceholder(String placeholderName) { String value = this.resolver.resolvePlaceholder(placeholderName); if (value != null && logger.isTraceEnabled()) { logger.trace("Resolved placeholder '" + placeholderName + "'"); @@ -451,8 +447,7 @@ public String text() { * @return the full resolution of the given {@code key} or {@code null} if * the placeholder has no value to begin with */ - @Nullable - protected String resolveRecursively(PartResolutionContext resolutionContext, String key) { + protected @Nullable String resolveRecursively(PartResolutionContext resolutionContext, String key) { String resolvedValue = resolutionContext.resolvePlaceholder(key); if (resolvedValue != null) { resolutionContext.flagPlaceholderAsVisited(key); @@ -509,8 +504,7 @@ static class SimplePlaceholderPart extends AbstractPart { private final String key; - @Nullable - private final String fallback; + private final @Nullable String fallback; /** * Create a new instance. @@ -557,8 +551,7 @@ static class NestedPlaceholderPart extends AbstractPart { private final List keyParts; - @Nullable - private final List defaultParts; + private final @Nullable List defaultParts; /** * Create a new instance. diff --git a/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java index 805467730188..1a28a371aec8 100644 --- a/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java +++ b/spring-core/src/main/java/org/springframework/util/PlaceholderResolutionException.java @@ -21,7 +21,7 @@ import java.util.List; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Thrown when the resolution of placeholder failed. This exception provides diff --git a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java index 78cb9a2c1ffc..ae73ab29737b 100644 --- a/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java +++ b/spring-core/src/main/java/org/springframework/util/PropertyPlaceholderHelper.java @@ -18,7 +18,7 @@ import java.util.Properties; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Utility class for working with Strings that have placeholder values in them. @@ -49,24 +49,6 @@ public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuf this(placeholderPrefix, placeholderSuffix, null, null, true); } - /** - * Create a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. - * @param placeholderPrefix the prefix that denotes the start of a placeholder - * @param placeholderSuffix the suffix that denotes the end of a placeholder - * @param valueSeparator the separating character between the placeholder variable - * and the associated default value, if any - * @param ignoreUnresolvablePlaceholders indicates whether unresolvable placeholders should - * be ignored ({@code true}) or cause an exception ({@code false}) - * @deprecated as of 6.2, in favor of - * {@link PropertyPlaceholderHelper#PropertyPlaceholderHelper(String, String, String, Character, boolean)} - */ - @Deprecated(since = "6.2", forRemoval = true) - public PropertyPlaceholderHelper(String placeholderPrefix, String placeholderSuffix, - @Nullable String valueSeparator, boolean ignoreUnresolvablePlaceholders) { - - this(placeholderPrefix, placeholderSuffix, valueSeparator, null, ignoreUnresolvablePlaceholders); - } - /** * Create a new {@code PropertyPlaceholderHelper} that uses the supplied prefix and suffix. * @param placeholderPrefix the prefix that denotes the start of a placeholder @@ -130,8 +112,7 @@ public interface PlaceholderResolver { * @param placeholderName the name of the placeholder to resolve * @return the replacement value, or {@code null} if no replacement is to be made */ - @Nullable - String resolvePlaceholder(String placeholderName); + @Nullable String resolvePlaceholder(String placeholderName); } } 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 39c537a008c9..80ad6c5ba3ea 100644 --- a/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java +++ b/spring-core/src/main/java/org/springframework/util/ReflectionUtils.java @@ -27,7 +27,9 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Simple utility class for working with the reflection API and handling @@ -137,6 +139,7 @@ public static void handleInvocationTargetException(InvocationTargetException ex) * @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 static void rethrowRuntimeException(@Nullable Throwable ex) { * @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; @@ -213,8 +217,7 @@ public static void makeAccessible(Constructor ctor) { * @param name the name of the method * @return the Method object, or {@code null} if none found */ - @Nullable - public static Method findMethod(Class clazz, String name) { + public static @Nullable Method findMethod(Class clazz, String name) { return findMethod(clazz, name, EMPTY_CLASS_ARRAY); } @@ -228,8 +231,7 @@ public static Method findMethod(Class clazz, String name) { * (may be {@code null} to indicate any signature) * @return the Method object, or {@code null} if none found */ - @Nullable - public static Method findMethod(Class clazz, String name, @Nullable Class... paramTypes) { + public static @Nullable Method findMethod(Class clazz, String name, Class @Nullable ... paramTypes) { Assert.notNull(clazz, "Class must not be null"); Assert.notNull(name, "Method name must not be null"); Class searchType = clazz; @@ -260,8 +262,7 @@ private static boolean hasSameParams(Method method, Class[] paramTypes) { * @return the invocation result, if any * @see #invokeMethod(java.lang.reflect.Method, Object, Object[]) */ - @Nullable - public static Object invokeMethod(Method method, @Nullable Object target) { + public static @Nullable Object invokeMethod(Method method, @Nullable Object target) { return invokeMethod(method, target, EMPTY_OBJECT_ARRAY); } @@ -275,8 +276,7 @@ public static Object invokeMethod(Method method, @Nullable Object target) { * @param args the invocation arguments (may be {@code null}) * @return the invocation result, if any */ - @Nullable - public static Object invokeMethod(Method method, @Nullable Object target, @Nullable Object... args) { + public static @Nullable Object invokeMethod(Method method, @Nullable Object target, @Nullable Object... args) { try { return method.invoke(target, args); } @@ -486,8 +486,7 @@ private static Method[] getDeclaredMethods(Class clazz, boolean defensive) { return (result.length == 0 || !defensive) ? result : result.clone(); } - @Nullable - private static List findDefaultMethodsOnInterfaces(Class clazz) { + private static @Nullable List findDefaultMethodsOnInterfaces(Class clazz) { List result = null; for (Class ifc : clazz.getInterfaces()) { for (Method method : ifc.getMethods()) { @@ -506,6 +505,7 @@ private static List findDefaultMethodsOnInterfaces(Class clazz) { * 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); @@ -515,6 +515,7 @@ public static boolean isEqualsMethod(@Nullable Method method) { * 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")); } @@ -523,6 +524,7 @@ public static boolean isHashCodeMethod(@Nullable Method method) { * 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")); } @@ -530,6 +532,7 @@ public static boolean isToStringMethod(@Nullable Method method) { /** * 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))); @@ -577,8 +580,7 @@ public static void makeAccessible(Method method) { * @param name the name of the field * @return the corresponding Field object, or {@code null} if not found */ - @Nullable - public static Field findField(Class clazz, String name) { + public static @Nullable Field findField(Class clazz, String name) { return findField(clazz, name, null); } @@ -591,8 +593,8 @@ public static Field findField(Class clazz, String name) { * @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 */ - @Nullable - public static Field findField(Class clazz, @Nullable String name, @Nullable Class type) { + @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"); Class searchType = clazz; @@ -617,8 +619,7 @@ public static Field findField(Class clazz, @Nullable String name, @Nullable C * @return the corresponding Field object, or {@code null} if not found * @since 6.1 */ - @Nullable - public static Field findFieldIgnoreCase(Class clazz, String name) { + public static @Nullable Field findFieldIgnoreCase(Class clazz, String name) { Assert.notNull(clazz, "Class must not be null"); Assert.notNull(name, "Name must not be null"); Class searchType = clazz; @@ -666,8 +667,7 @@ public static void setField(Field field, @Nullable Object target, @Nullable Obje * (or {@code null} for a static field) * @return the field's current value */ - @Nullable - public static Object getField(Field field, @Nullable Object target) { + public static @Nullable Object getField(Field field, @Nullable Object target) { try { return field.get(target); } 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 9a2f12f1cdbf..ea544470caf6 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. @@ -26,7 +26,9 @@ import java.net.URLConnection; import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Utility methods for resolving resource locations to files in the @@ -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/RouteMatcher.java b/spring-core/src/main/java/org/springframework/util/RouteMatcher.java index 166f9e677d03..3011300cb7a5 100644 --- a/spring-core/src/main/java/org/springframework/util/RouteMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/RouteMatcher.java @@ -19,7 +19,7 @@ import java.util.Comparator; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Contract for matching routes to patterns. @@ -74,8 +74,7 @@ public interface RouteMatcher { * @param route the route to extract template variables from * @return a map with template variables and values */ - @Nullable - Map matchAndExtract(String pattern, Route route); + @Nullable Map matchAndExtract(String pattern, Route route); /** * Given a route, return a {@link Comparator} suitable for sorting patterns 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 1eb905a785f0..79c58b1e7b25 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. @@ -23,7 +23,9 @@ import java.io.ObjectOutputStream; import java.io.Serializable; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Static utilities for serialization and deserialization using @@ -47,8 +49,8 @@ public abstract class SerializationUtils { * @param object the object to serialize * @return an array of bytes representing the object in a portable fashion */ - @Nullable - public static byte[] serialize(@Nullable Object object) { + @Contract("null -> null") + public static byte @Nullable [] serialize(@Nullable Object object) { if (object == null) { return null; } @@ -73,9 +75,9 @@ public static byte[] serialize(@Nullable Object object) { *

    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 - @Nullable - public static Object deserialize(@Nullable byte[] bytes) { + @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/SimpleRouteMatcher.java b/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java index ead414e1f3ef..dbb3710e4913 100644 --- a/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java +++ b/spring-core/src/main/java/org/springframework/util/SimpleRouteMatcher.java @@ -19,7 +19,7 @@ import java.util.Comparator; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code RouteMatcher} that delegates to a {@link PathMatcher}. @@ -75,8 +75,7 @@ public boolean match(String pattern, Route route) { } @Override - @Nullable - public Map matchAndExtract(String pattern, Route route) { + public @Nullable Map matchAndExtract(String pattern, Route route) { if (!match(pattern, route)) { return null; } diff --git a/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java b/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java index f02bb4ee9429..013e9d0a0ba9 100644 --- a/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/SingleToMultiValueMapAdapter.java @@ -28,7 +28,7 @@ import java.util.Set; import java.util.function.BiConsumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Adapts a given {@link MultiValueMap} to the {@link Map} contract. The @@ -47,11 +47,9 @@ final class SingleToMultiValueMapAdapter implements MultiValueMap, S private final Map targetMap; - @Nullable - private transient Collection> values; + private transient @Nullable Collection> values; - @Nullable - private transient Set>> entries; + private transient @Nullable Set>> entries; /** @@ -67,8 +65,7 @@ public SingleToMultiValueMapAdapter(Map targetMap) { // MultiValueMap implementation @Override - @Nullable - public V getFirst(K key) { + public @Nullable V getFirst(K key) { return this.targetMap.get(key); } @@ -154,15 +151,13 @@ public boolean containsValue(@Nullable Object value) { } @Override - @Nullable - public List get(Object key) { + public @Nullable List get(Object key) { V value = this.targetMap.get(key); return (value != null) ? Collections.singletonList(value) : null; } @Override - @Nullable - public List put(K key, List values) { + public @Nullable List put(K key, List values) { if (values.isEmpty()) { V result = this.targetMap.put(key, null); return (result != null) ? Collections.singletonList(result) : null; @@ -177,8 +172,7 @@ else if (values.size() == 1) { } @Override - @Nullable - public List remove(Object key) { + public @Nullable List remove(Object key) { V result = this.targetMap.remove(key); return (result != null) ? Collections.singletonList(result) : null; } diff --git a/spring-core/src/main/java/org/springframework/util/StopWatch.java b/spring-core/src/main/java/org/springframework/util/StopWatch.java index 49b888dd938f..1211f0388cdb 100644 --- a/spring-core/src/main/java/org/springframework/util/StopWatch.java +++ b/spring-core/src/main/java/org/springframework/util/StopWatch.java @@ -22,7 +22,7 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple stop watch, allowing for timing of a number of tasks, exposing total @@ -60,18 +60,15 @@ public class StopWatch { */ private final String id; - @Nullable - private List taskList = new ArrayList<>(1); + private @Nullable List taskList = new ArrayList<>(1); /** Start time of the current task. */ private long startTimeNanos; /** Name of the current task. */ - @Nullable - private String currentTaskName; + private @Nullable String currentTaskName; - @Nullable - private TaskInfo lastTaskInfo; + private @Nullable TaskInfo lastTaskInfo; private int taskCount; @@ -181,8 +178,7 @@ public boolean isRunning() { * @since 4.2.2 * @see #isRunning() */ - @Nullable - public String currentTaskName() { + public @Nullable String currentTaskName() { return this.currentTaskName; } 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 99b44e03f29e..c35c7599ab59 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. @@ -25,13 +25,14 @@ import java.io.OutputStream; import java.nio.charset.Charset; -import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** - * 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. * @@ -190,14 +191,14 @@ public static long copyRange(InputStream in, OutputStream out, long start, long } /** - * 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; @@ -239,7 +240,7 @@ public static OutputStream nonClosing(OutputStream out) { } - private static class NonClosingInputStream extends FilterInputStream { + private static final class NonClosingInputStream extends FilterInputStream { public NonClosingInputStream(InputStream in) { super(in); @@ -248,10 +249,30 @@ public NonClosingInputStream(InputStream in) { @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 deed77ff405e..a8d63b1d2813 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.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; @@ -36,8 +36,9 @@ import java.util.TimeZone; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; /** * Miscellaneous {@link String} utility methods. @@ -104,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)); } @@ -209,6 +211,7 @@ public static boolean hasText(@Nullable String str) { * contains at least 1 whitespace character * @see Character#isWhitespace */ + @Contract("null -> false") public static boolean containsWhitespace(@Nullable CharSequence str) { if (!hasLength(str)) { return false; @@ -230,6 +233,7 @@ public static boolean containsWhitespace(@Nullable CharSequence str) { * contains at least 1 whitespace character * @see #containsWhitespace(CharSequence) */ + @Contract("null -> false") public static boolean containsWhitespace(@Nullable String str) { return containsWhitespace((CharSequence) str); } @@ -365,6 +369,7 @@ public static String trimTrailingCharacter(String str, char trailingCharacter) { * @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); } @@ -376,6 +381,7 @@ public static boolean matchesCharacter(@Nullable String str, char singleCharacte * @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())); @@ -388,6 +394,7 @@ public static boolean startsWithIgnoreCase(@Nullable String str, @Nullable Strin * @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())); @@ -515,8 +522,8 @@ public static String deleteAny(String inString, @Nullable String charsToDelete) * @return the quoted {@code String} (for example, "'myString'"), * or {@code null} if the input was {@code null} */ - @Nullable - public static String quote(@Nullable String str) { + @Contract("null -> null; !null -> !null") + public static @Nullable String quote(@Nullable String str) { return (str != null ? "'" + str + "'" : null); } @@ -527,8 +534,8 @@ public static String quote(@Nullable String str) { * @return the quoted {@code String} (for example, "'myString'"), * or the input object as-is if not a {@code String} */ - @Nullable - public static Object quoteIfString(@Nullable Object obj) { + @Contract("null -> null; !null -> !null") + public static @Nullable Object quoteIfString(@Nullable Object obj) { return (obj instanceof String str ? quote(str) : obj); } @@ -614,13 +621,21 @@ private static String changeFirstCharacterCase(String str, boolean capitalize) { } /** - * 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} */ - @Nullable - public static String getFilename(@Nullable String path) { + @Contract("null -> null; !null -> !null") + public static @Nullable String getFilename(@Nullable String path) { if (path == null) { return null; } @@ -630,13 +645,23 @@ public static String getFilename(@Nullable String path) { } /** - * 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 "."}) */ - @Nullable - public static String getFilenameExtension(@Nullable String path) { + @Contract("null -> null") + public static @Nullable String getFilenameExtension(@Nullable String path) { if (path == null) { return null; } @@ -803,54 +828,64 @@ public static boolean pathEquals(String path1, String path2) { } /** - * 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]; + } + + 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); + } } - baos.write((char) ((u << 4) + l)); - i += 2; - changed = true; + + 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(); } /** @@ -866,8 +901,7 @@ public static String uriDecode(String source, Charset charset) { * @see #parseLocaleString * @see Locale#forLanguageTag */ - @Nullable - public static Locale parseLocale(String localeValue) { + public static @Nullable Locale parseLocale(String localeValue) { if (!localeValue.contains("_") && !localeValue.contains(" ")) { validateLocalePart(localeValue); Locale resolved = Locale.forLanguageTag(localeValue); @@ -893,8 +927,7 @@ public static Locale parseLocale(String localeValue) { * @throws IllegalArgumentException in case of an invalid locale specification */ @SuppressWarnings("deprecation") // for Locale constructors on JDK 19 - @Nullable - public static Locale parseLocaleString(String localeString) { + public static @Nullable Locale parseLocaleString(String localeString) { if (localeString.isEmpty()) { return null; } @@ -990,7 +1023,7 @@ public static String[] toStringArray(@Nullable Enumeration enumeration) * @param str the {@code String} to append * @return the new array (never {@code null}) */ - public static String[] addStringToArray(@Nullable String[] array, String str) { + public static String[] addStringToArray(String @Nullable [] array, String str) { if (ObjectUtils.isEmpty(array)) { return new String[] {str}; } @@ -1009,8 +1042,8 @@ public static String[] addStringToArray(@Nullable String[] array, String str) { * @param array2 the second array (can be {@code null}) * @return the new array ({@code null} if both given arrays were {@code null}) */ - @Nullable - public static String[] concatenateStringArrays(@Nullable String[] array1, @Nullable String[] array2) { + @Contract("null, _ -> param2; _, null -> param1") + public static String @Nullable [] concatenateStringArrays(String @Nullable [] array1, String @Nullable [] array2) { if (ObjectUtils.isEmpty(array1)) { return array2; } @@ -1044,12 +1077,12 @@ public static String[] sortStringArray(String[] array) { * @param array the original {@code String} array (potentially empty) * @return the resulting array (of the same size) with trimmed elements */ - public static String[] trimArrayElements(String[] array) { + public static @Nullable String[] trimArrayElements(@Nullable String[] array) { if (ObjectUtils.isEmpty(array)) { return array; } - String[] result = new String[array.length]; + @Nullable String[] result = new String[array.length]; for (int i = 0; i < array.length; i++) { String element = array[i]; result[i] = (element != null ? element.trim() : null); @@ -1081,8 +1114,8 @@ public static String[] removeDuplicateStrings(String[] array) { * 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} */ - @Nullable - public static String[] split(@Nullable String toSplit, @Nullable String delimiter) { + @Contract("null, _ -> null; _, null -> null") + public static String @Nullable [] split(@Nullable String toSplit, @Nullable String delimiter) { if (!hasLength(toSplit) || !hasLength(delimiter)) { return null; } @@ -1106,8 +1139,7 @@ public static String[] split(@Nullable String toSplit, @Nullable String delimite * @return a {@code Properties} instance representing the array contents, * or {@code null} if the array to process was {@code null} or empty */ - @Nullable - public static Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { + public static @Nullable Properties splitArrayElementsIntoProperties(String[] array, String delimiter) { return splitArrayElementsIntoProperties(array, delimiter, null); } @@ -1125,9 +1157,9 @@ public static Properties splitArrayElementsIntoProperties(String[] array, String * @return a {@code Properties} instance representing the array contents, * or {@code null} if the array to process was {@code null} or empty */ - @Nullable - public static Properties splitArrayElementsIntoProperties( - String[] array, String delimiter, @Nullable String charsToDelete) { + @Contract("null, _, _ -> null") + public static @Nullable Properties splitArrayElementsIntoProperties( + String @Nullable [] array, String delimiter, @Nullable String charsToDelete) { if (ObjectUtils.isEmpty(array)) { return null; @@ -1353,7 +1385,7 @@ public static String collectionToCommaDelimitedString(@Nullable Collection co * @param delim the delimiter to use (typically a ",") * @return the delimited {@code String} */ - public static String arrayToDelimitedString(@Nullable Object[] arr, String delim) { + public static String arrayToDelimitedString(@Nullable Object @Nullable [] arr, String delim) { if (ObjectUtils.isEmpty(arr)) { return ""; } @@ -1375,7 +1407,7 @@ public static String arrayToDelimitedString(@Nullable Object[] arr, String delim * @param arr the array to display (potentially {@code null} or empty) * @return the delimited {@code String} */ - public static String arrayToCommaDelimitedString(@Nullable Object[] arr) { + public static String arrayToCommaDelimitedString(@Nullable Object @Nullable [] arr) { return arrayToDelimitedString(arr, ","); } diff --git a/spring-core/src/main/java/org/springframework/util/StringValueResolver.java b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java index 1335de95867f..822840af4669 100644 --- a/spring-core/src/main/java/org/springframework/util/StringValueResolver.java +++ b/spring-core/src/main/java/org/springframework/util/StringValueResolver.java @@ -16,7 +16,7 @@ package org.springframework.util; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple strategy interface for resolving a String value. @@ -38,7 +38,6 @@ public interface StringValueResolver { * to resolve or when ignoring unresolvable placeholders) * @throws IllegalArgumentException in case of an unresolvable String value */ - @Nullable - String resolveStringValue(String strVal); + @Nullable String resolveStringValue(String strVal); } 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 eb962efc5bb3..c0f5b2ae3e43 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. @@ -16,7 +16,7 @@ package org.springframework.util; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Helper class for resolving placeholders in texts. Usually applied to file paths. @@ -35,16 +35,19 @@ */ 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 = '\\'; @@ -104,8 +107,7 @@ public SystemPropertyPlaceholderResolver(String text) { } @Override - @Nullable - public String resolvePlaceholder(String placeholderName) { + public @Nullable String resolvePlaceholder(String placeholderName) { try { String propVal = System.getProperty(placeholderName); if (propVal == null) { 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 b118aa2eeef0..c14007b5388e 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. @@ -21,7 +21,9 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Utility to work with generic type parameters. @@ -210,6 +212,7 @@ private static Type[] getUpperBounds(WildcardType wildcardType) { 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/UnmodifiableMultiValueMap.java b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java index 9a14150156ca..7cd5f5ab154c 100644 --- a/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java +++ b/spring-core/src/main/java/org/springframework/util/UnmodifiableMultiValueMap.java @@ -33,7 +33,7 @@ import java.util.stream.Stream; import java.util.stream.StreamSupport; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Unmodifiable wrapper for {@link MultiValueMap}. @@ -47,16 +47,14 @@ final class UnmodifiableMultiValueMap implements MultiValueMap, Serial private static final long serialVersionUID = -8697084563854098920L; + @SuppressWarnings("serial") private final MultiValueMap delegate; - @Nullable - private transient Set keySet; + private transient @Nullable Set keySet; - @Nullable - private transient Set>> entrySet; + private transient @Nullable Set>> entrySet; - @Nullable - private transient Collection> values; + private transient @Nullable Collection> values; @SuppressWarnings("unchecked") @@ -89,15 +87,13 @@ public boolean containsValue(Object value) { } @Override - @Nullable - public List get(Object key) { + public @Nullable List get(Object key) { List result = this.delegate.get(key); return (result != null ? Collections.unmodifiableList(result) : null); } @Override - @Nullable - public V getFirst(K key) { + public @Nullable V getFirst(K key) { return this.delegate.getFirst(key); } @@ -169,9 +165,8 @@ public Collection> values() { // unsupported - @Nullable @Override - public List put(K key, List value) { + public @Nullable List put(K key, List value) { throw new UnsupportedOperationException(); } @@ -270,6 +265,7 @@ private static class UnmodifiableEntrySet implements Set>> delegate; @SuppressWarnings("unchecked") @@ -433,8 +429,7 @@ public void forEachRemaining(Consumer>> action) { } @Override - @Nullable - public Spliterator>> trySplit() { + public @Nullable Spliterator>> trySplit() { Spliterator>> split = this.delegate.trySplit(); if (split != null) { return new UnmodifiableEntrySpliterator<>(split); @@ -519,6 +514,7 @@ private static class UnmodifiableValueCollection implements Collection> delegate; public UnmodifiableValueCollection(Collection> delegate) { @@ -677,8 +673,7 @@ public void forEachRemaining(Consumer> action) { } @Override - @Nullable - public Spliterator> trySplit() { + public @Nullable Spliterator> trySplit() { Spliterator> split = this.delegate.trySplit(); if (split != null) { return new UnmodifiableValueSpliterator<>(split); diff --git a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java index 5cd39685fa28..79836518a165 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/ExponentialBackOff.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -85,6 +85,7 @@ public class ExponentialBackOff implements BackOff { */ public static final int DEFAULT_MAX_ATTEMPTS = Integer.MAX_VALUE; + private long initialInterval = DEFAULT_INITIAL_INTERVAL; private double multiplier = DEFAULT_MULTIPLIER; @@ -204,6 +205,7 @@ public int getMaxAttempts() { return this.maxAttempts; } + @Override public BackOffExecution start() { return new ExponentialBackOffExecution(); @@ -225,6 +227,7 @@ public String toString() { .toString(); } + private class ExponentialBackOffExecution implements BackOffExecution { private long currentInterval = -1; diff --git a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java index b4d80c481227..9695077362b1 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/FixedBackOff.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2018 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,6 +35,7 @@ public class FixedBackOff implements BackOff { */ public static final long UNLIMITED_ATTEMPTS = Long.MAX_VALUE; + private long interval = DEFAULT_INTERVAL; private long maxAttempts = UNLIMITED_ATTEMPTS; @@ -86,6 +87,7 @@ public long getMaxAttempts() { return this.maxAttempts; } + @Override public BackOffExecution start() { return new FixedBackOffExecution(); diff --git a/spring-core/src/main/java/org/springframework/util/backoff/package-info.java b/spring-core/src/main/java/org/springframework/util/backoff/package-info.java index 071542a7a1b4..c4e680b5a1e5 100644 --- a/spring-core/src/main/java/org/springframework/util/backoff/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/backoff/package-info.java @@ -1,9 +1,7 @@ /** * A generic back-off abstraction. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util.backoff; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java index 6f65f92d5b72..f3626f84b282 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/BooleanComparator.java @@ -19,7 +19,7 @@ import java.io.Serializable; import java.util.Comparator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@link Comparator} for {@link Boolean} objects that can sort either diff --git a/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java index 66d4b4f37df7..e56384cedc4f 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/InstanceComparator.java @@ -18,7 +18,8 @@ import java.util.Comparator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java index 77dce7608270..3c89ec971358 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/NullSafeComparator.java @@ -18,7 +18,8 @@ import java.util.Comparator; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-core/src/main/java/org/springframework/util/comparator/package-info.java b/spring-core/src/main/java/org/springframework/util/comparator/package-info.java index 3d4ebd53b7f7..7fae7e8e4c23 100644 --- a/spring-core/src/main/java/org/springframework/util/comparator/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/comparator/package-info.java @@ -2,9 +2,7 @@ * Useful generic {@code java.util.Comparator} implementations, * such as an invertible comparator and a compound comparator. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util.comparator; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java deleted file mode 100644 index 0c50053c327f..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/CompletableToListenableFutureAdapter.java +++ /dev/null @@ -1,110 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -/** - * Adapts a {@link CompletableFuture} or {@link CompletionStage} into a - * Spring {@link ListenableFuture}. - * - * @author Sebastien Deleuze - * @author Juergen Hoeller - * @since 4.2 - * @param the result type returned by this Future's {@code get} method - * @deprecated as of 6.0, with no concrete replacement - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public class CompletableToListenableFutureAdapter implements ListenableFuture { - - private final CompletableFuture completableFuture; - - private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry<>(); - - - /** - * Create a new adapter for the given {@link CompletionStage}. - * @since 4.3.7 - */ - public CompletableToListenableFutureAdapter(CompletionStage completionStage) { - this(completionStage.toCompletableFuture()); - } - - /** - * Create a new adapter for the given {@link CompletableFuture}. - */ - public CompletableToListenableFutureAdapter(CompletableFuture completableFuture) { - this.completableFuture = completableFuture; - this.completableFuture.whenComplete((result, ex) -> { - if (ex != null) { - this.callbacks.failure(ex); - } - else { - this.callbacks.success(result); - } - }); - } - - - @Override - public void addCallback(ListenableFutureCallback callback) { - this.callbacks.addCallback(callback); - } - - @Override - public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { - this.callbacks.addSuccessCallback(successCallback); - this.callbacks.addFailureCallback(failureCallback); - } - - @Override - public CompletableFuture completable() { - return this.completableFuture; - } - - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - return this.completableFuture.cancel(mayInterruptIfRunning); - } - - @Override - public boolean isCancelled() { - return this.completableFuture.isCancelled(); - } - - @Override - public boolean isDone() { - return this.completableFuture.isDone(); - } - - @Override - public T get() throws InterruptedException, ExecutionException { - return this.completableFuture.get(); - } - - @Override - public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return this.completableFuture.get(timeout, unit); - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java deleted file mode 100644 index e24adfdd6f78..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/FailureCallback.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.function.BiConsumer; - -/** - * Failure callback for a {@link ListenableFuture}. - * - * @author Sebastien Deleuze - * @since 4.1 - * @deprecated as of 6.0, in favor of - * {@link java.util.concurrent.CompletableFuture#whenComplete(BiConsumer)} - */ -@Deprecated(since = "6.0", forRemoval = true) -@FunctionalInterface -public interface FailureCallback { - - /** - * Called when the {@link ListenableFuture} completes with failure. - *

    Note that Exceptions raised by this method are ignored. - * @param ex the failure - */ - void onFailure(Throwable ex); - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java index 43afe42b3006..8864d4477cf3 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/FutureAdapter.java @@ -21,7 +21,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -40,8 +41,7 @@ public abstract class FutureAdapter implements Future { private final Future adaptee; - @Nullable - private Object result; + private @Nullable Object result; private State state = State.NEW; @@ -81,20 +81,17 @@ public boolean isDone() { } @Override - @Nullable - public T get() throws InterruptedException, ExecutionException { + public @Nullable T get() throws InterruptedException, ExecutionException { return adaptInternal(this.adaptee.get()); } @Override - @Nullable - public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { + public @Nullable T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return adaptInternal(this.adaptee.get(timeout, unit)); } @SuppressWarnings("unchecked") - @Nullable - final T adaptInternal(S adapteeResult) throws ExecutionException { + final @Nullable T adaptInternal(S adapteeResult) throws ExecutionException { synchronized (this.mutex) { return switch (this.state) { case SUCCESS -> (T) this.result; @@ -129,8 +126,7 @@ final T adaptInternal(S adapteeResult) throws ExecutionException { * Adapts the given adaptee's result into T. * @return the adapted result */ - @Nullable - protected abstract T adapt(S adapteeResult) throws ExecutionException; + protected abstract @Nullable T adapt(S adapteeResult) throws ExecutionException; private enum State {NEW, SUCCESS, FAILURE} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java deleted file mode 100644 index fc8c0e5f2072..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFuture.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.util.concurrent; - -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.Future; -import java.util.function.BiConsumer; - -/** - * Extend {@link Future} with the capability to accept completion callbacks. - * If the future has completed when the callback is added, the callback is - * triggered immediately. - * - *

    Inspired by {@code com.google.common.util.concurrent.ListenableFuture}. - * - * @author Arjen Poutsma - * @author Sebastien Deleuze - * @author Juergen Hoeller - * @since 4.0 - * @param the result type returned by this Future's {@code get} method - * @deprecated as of 6.0, in favor of {@link CompletableFuture} - */ -@Deprecated(since = "6.0", forRemoval = true) -public interface ListenableFuture extends Future { - - /** - * Register the given {@code ListenableFutureCallback}. - * @param callback the callback to register - * @deprecated as of 6.0, in favor of - * {@link CompletableFuture#whenComplete(BiConsumer)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - void addCallback(ListenableFutureCallback callback); - - /** - * Java 8 lambda-friendly alternative with success and failure callbacks. - * @param successCallback the success callback - * @param failureCallback the failure callback - * @since 4.1 - * @deprecated as of 6.0, in favor of - * {@link CompletableFuture#whenComplete(BiConsumer)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - void addCallback(SuccessCallback successCallback, FailureCallback failureCallback); - - - /** - * Expose this {@link ListenableFuture} as a JDK {@link CompletableFuture}. - * @since 5.0 - */ - @SuppressWarnings("NullAway") - default CompletableFuture completable() { - CompletableFuture completable = new DelegatingCompletableFuture<>(this); - addCallback(completable::complete, completable::completeExceptionally); - return completable; - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java deleted file mode 100644 index 0ddc5bc42908..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureAdapter.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.concurrent.ExecutionException; - -import org.springframework.lang.Nullable; - -/** - * Abstract class that adapts a {@link ListenableFuture} parameterized over S into a - * {@code ListenableFuture} parameterized over T. All methods are delegated to the - * adaptee, where {@link #get()}, {@link #get(long, java.util.concurrent.TimeUnit)}, - * and {@link ListenableFutureCallback#onSuccess(Object)} call {@link #adapt(Object)} - * on the adaptee's result. - * - * @author Arjen Poutsma - * @since 4.0 - * @param the type of this {@code Future} - * @param the type of the adaptee's {@code Future} - * @deprecated as of 6.0, in favor of - * {@link java.util.concurrent.CompletableFuture} - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public abstract class ListenableFutureAdapter extends FutureAdapter implements ListenableFuture { - - /** - * Construct a new {@code ListenableFutureAdapter} with the given adaptee. - * @param adaptee the future to adapt to - */ - protected ListenableFutureAdapter(ListenableFuture adaptee) { - super(adaptee); - } - - - @Override - public void addCallback(final ListenableFutureCallback callback) { - addCallback(callback, callback); - } - - @Override - public void addCallback(final SuccessCallback successCallback, final FailureCallback failureCallback) { - ListenableFuture listenableAdaptee = (ListenableFuture) getAdaptee(); - listenableAdaptee.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(@Nullable S result) { - T adapted = null; - if (result != null) { - try { - adapted = adaptInternal(result); - } - catch (ExecutionException ex) { - Throwable cause = ex.getCause(); - onFailure(cause != null ? cause : ex); - return; - } - catch (Throwable ex) { - onFailure(ex); - return; - } - } - successCallback.onSuccess(adapted); - } - - @Override - public void onFailure(Throwable ex) { - failureCallback.onFailure(ex); - } - }); - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java deleted file mode 100644 index dfd0f4e24480..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallback.java +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.function.BiConsumer; - -/** - * Callback mechanism for the outcome, success or failure, from a - * {@link ListenableFuture}. - * - * @author Arjen Poutsma - * @author Sebastien Deleuze - * @since 4.0 - * @param the result type - * @deprecated as of 6.0, in favor of - * {@link java.util.concurrent.CompletableFuture#whenComplete(BiConsumer)} - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public interface ListenableFutureCallback extends SuccessCallback, FailureCallback { - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java deleted file mode 100644 index 178d549c162c..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureCallbackRegistry.java +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.ArrayDeque; -import java.util.Queue; - -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * Helper class for {@link ListenableFuture} implementations that maintains a queue - * of success and failure callbacks and helps to notify them. - * - *

    Inspired by {@code com.google.common.util.concurrent.ExecutionList}. - * - * @author Arjen Poutsma - * @author Sebastien Deleuze - * @author Rossen Stoyanchev - * @since 4.0 - * @param the callback result type - * @deprecated as of 6.0, with no concrete replacement - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public class ListenableFutureCallbackRegistry { - - private final Queue> successCallbacks = new ArrayDeque<>(1); - - private final Queue failureCallbacks = new ArrayDeque<>(1); - - private State state = State.NEW; - - @Nullable - private Object result; - - private final Object mutex = new Object(); - - - /** - * Add the given callback to this registry. - * @param callback the callback to add - */ - public void addCallback(ListenableFutureCallback callback) { - Assert.notNull(callback, "'callback' must not be null"); - synchronized (this.mutex) { - switch (this.state) { - case NEW -> { - this.successCallbacks.add(callback); - this.failureCallbacks.add(callback); - } - case SUCCESS -> notifySuccess(callback); - case FAILURE -> notifyFailure(callback); - } - } - } - - @SuppressWarnings("unchecked") - private void notifySuccess(SuccessCallback callback) { - try { - callback.onSuccess((T) this.result); - } - catch (Throwable ex) { - // Ignore - } - } - - private void notifyFailure(FailureCallback callback) { - Assert.state(this.result instanceof Throwable, "No Throwable result for failure state"); - try { - callback.onFailure((Throwable) this.result); - } - catch (Throwable ex) { - // Ignore - } - } - - /** - * Add the given success callback to this registry. - * @param callback the success callback to add - * @since 4.1 - */ - public void addSuccessCallback(SuccessCallback callback) { - Assert.notNull(callback, "'callback' must not be null"); - synchronized (this.mutex) { - switch (this.state) { - case NEW -> this.successCallbacks.add(callback); - case SUCCESS -> notifySuccess(callback); - } - } - } - - /** - * Add the given failure callback to this registry. - * @param callback the failure callback to add - * @since 4.1 - */ - public void addFailureCallback(FailureCallback callback) { - Assert.notNull(callback, "'callback' must not be null"); - synchronized (this.mutex) { - switch (this.state) { - case NEW -> this.failureCallbacks.add(callback); - case FAILURE -> notifyFailure(callback); - } - } - } - - /** - * Trigger a {@link ListenableFutureCallback#onSuccess(Object)} call on all - * added callbacks with the given result. - * @param result the result to trigger the callbacks with - */ - public void success(@Nullable T result) { - synchronized (this.mutex) { - this.state = State.SUCCESS; - this.result = result; - SuccessCallback callback; - while ((callback = this.successCallbacks.poll()) != null) { - notifySuccess(callback); - } - } - } - - /** - * Trigger a {@link ListenableFutureCallback#onFailure(Throwable)} call on all - * added callbacks with the given {@code Throwable}. - * @param ex the exception to trigger the callbacks with - */ - public void failure(Throwable ex) { - synchronized (this.mutex) { - this.state = State.FAILURE; - this.result = ex; - FailureCallback callback; - while ((callback = this.failureCallbacks.poll()) != null) { - notifyFailure(callback); - } - } - } - - - private enum State {NEW, SUCCESS, FAILURE} - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java b/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java deleted file mode 100644 index 271fc083263c..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/ListenableFutureTask.java +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.FutureTask; - -import org.springframework.lang.Nullable; - -/** - * Extension of {@link FutureTask} that implements {@link ListenableFuture}. - * - * @author Arjen Poutsma - * @since 4.0 - * @param the result type returned by this Future's {@code get} method - * @deprecated as of 6.0, with no concrete replacement - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public class ListenableFutureTask extends FutureTask implements ListenableFuture { - - private final ListenableFutureCallbackRegistry callbacks = new ListenableFutureCallbackRegistry<>(); - - - /** - * Create a new {@code ListenableFutureTask} that will, upon running, - * execute the given {@link Callable}. - * @param callable the callable task - */ - public ListenableFutureTask(Callable callable) { - super(callable); - } - - /** - * Create a {@code ListenableFutureTask} that will, upon running, - * execute the given {@link Runnable}, and arrange that {@link #get()} - * will return the given result on successful completion. - * @param runnable the runnable task - * @param result the result to return on successful completion - */ - public ListenableFutureTask(Runnable runnable, @Nullable T result) { - super(runnable, result); - } - - - @Override - public void addCallback(ListenableFutureCallback callback) { - this.callbacks.addCallback(callback); - } - - @Override - public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { - this.callbacks.addSuccessCallback(successCallback); - this.callbacks.addFailureCallback(failureCallback); - } - - @Override - @SuppressWarnings("NullAway") - public CompletableFuture completable() { - CompletableFuture completable = new DelegatingCompletableFuture<>(this); - this.callbacks.addSuccessCallback(completable::complete); - this.callbacks.addFailureCallback(completable::completeExceptionally); - return completable; - } - - - @Override - protected void done() { - Throwable cause; - try { - T result = get(); - this.callbacks.success(result); - return; - } - catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - return; - } - catch (ExecutionException ex) { - cause = ex.getCause(); - if (cause == null) { - cause = ex; - } - } - catch (Throwable ex) { - cause = ex; - } - this.callbacks.failure(cause); - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java b/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java deleted file mode 100644 index f55162a08569..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/MonoToListenableFutureAdapter.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.util.concurrent; - -import reactor.core.publisher.Mono; - -/** - * Adapts a {@link Mono} into a {@link ListenableFuture} by obtaining a - * {@code CompletableFuture} from the {@code Mono} via {@link Mono#toFuture()} - * and then adapting it with {@link CompletableToListenableFutureAdapter}. - * - * @author Rossen Stoyanchev - * @author Stephane Maldini - * @since 5.1 - * @param the object type - * @deprecated as of 6.0, in favor of {@link Mono#toFuture()} - */ -@Deprecated(since = "6.0") -@SuppressWarnings("removal") -public class MonoToListenableFutureAdapter extends CompletableToListenableFutureAdapter { - - public MonoToListenableFutureAdapter(Mono mono) { - super(mono.toFuture()); - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java b/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java deleted file mode 100644 index e7cb0c755672..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/SettableListenableFuture.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.concurrent.Callable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.springframework.lang.Nullable; -import org.springframework.util.Assert; - -/** - * A {@link ListenableFuture} whose value can be set via {@link #set(Object)} - * or {@link #setException(Throwable)}. It may also get cancelled. - * - *

    Inspired by {@code com.google.common.util.concurrent.SettableFuture}. - * - * @author Mattias Severson - * @author Rossen Stoyanchev - * @author Juergen Hoeller - * @since 4.1 - * @param the result type returned by this Future's {@code get} method - * @deprecated as of 6.0, in favor of {@link CompletableFuture} - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public class SettableListenableFuture implements ListenableFuture { - - private static final Callable DUMMY_CALLABLE = () -> { - throw new IllegalStateException("Should never be called"); - }; - - - private final SettableTask settableTask = new SettableTask<>(); - - - /** - * Set the value of this future. This method will return {@code true} if the - * value was set successfully, or {@code false} if the future has already been - * set or cancelled. - * @param value the value that will be set - * @return {@code true} if the value was successfully set, else {@code false} - */ - public boolean set(@Nullable T value) { - return this.settableTask.setResultValue(value); - } - - /** - * Set the exception of this future. This method will return {@code true} if the - * exception was set successfully, or {@code false} if the future has already been - * set or cancelled. - * @param exception the value that will be set - * @return {@code true} if the exception was successfully set, else {@code false} - */ - public boolean setException(Throwable exception) { - Assert.notNull(exception, "Exception must not be null"); - return this.settableTask.setExceptionResult(exception); - } - - - @Override - public void addCallback(ListenableFutureCallback callback) { - this.settableTask.addCallback(callback); - } - - @Override - public void addCallback(SuccessCallback successCallback, FailureCallback failureCallback) { - this.settableTask.addCallback(successCallback, failureCallback); - } - - @Override - public CompletableFuture completable() { - return this.settableTask.completable(); - } - - - @Override - public boolean cancel(boolean mayInterruptIfRunning) { - boolean cancelled = this.settableTask.cancel(mayInterruptIfRunning); - if (cancelled && mayInterruptIfRunning) { - interruptTask(); - } - return cancelled; - } - - @Override - public boolean isCancelled() { - return this.settableTask.isCancelled(); - } - - @Override - public boolean isDone() { - return this.settableTask.isDone(); - } - - /** - * Retrieve the value. - *

    This method returns the value if it has been set via {@link #set(Object)}, - * throws an {@link java.util.concurrent.ExecutionException} if an exception has - * been set via {@link #setException(Throwable)}, or throws a - * {@link java.util.concurrent.CancellationException} if the future has been cancelled. - * @return the value associated with this future - */ - @Nullable - @Override - public T get() throws InterruptedException, ExecutionException { - return this.settableTask.get(); - } - - /** - * Retrieve the value. - *

    This method returns the value if it has been set via {@link #set(Object)}, - * throws an {@link java.util.concurrent.ExecutionException} if an exception has - * been set via {@link #setException(Throwable)}, or throws a - * {@link java.util.concurrent.CancellationException} if the future has been cancelled. - * @param timeout the maximum time to wait - * @param unit the unit of the timeout argument - * @return the value associated with this future - */ - @Nullable - @Override - public T get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { - return this.settableTask.get(timeout, unit); - } - - /** - * Subclasses can override this method to implement interruption of the future's - * computation. The method is invoked automatically by a successful call to - * {@link #cancel(boolean) cancel(true)}. - *

    The default implementation is empty. - */ - protected void interruptTask() { - } - - - private static class SettableTask extends ListenableFutureTask { - - @Nullable - private volatile Thread completingThread; - - @SuppressWarnings("unchecked") - public SettableTask() { - super((Callable) DUMMY_CALLABLE); - } - - public boolean setResultValue(@Nullable T value) { - set(value); - return checkCompletingThread(); - } - - public boolean setExceptionResult(Throwable exception) { - setException(exception); - return checkCompletingThread(); - } - - @Override - protected void done() { - if (!isCancelled()) { - // Implicitly invoked by set/setException: store current thread for - // determining whether the given result has actually triggered completion - // (since FutureTask.set/setException unfortunately don't expose that) - this.completingThread = Thread.currentThread(); - } - super.done(); - } - - private boolean checkCompletingThread() { - boolean check = (this.completingThread == Thread.currentThread()); - if (check) { - this.completingThread = null; // only first match actually counts - } - return check; - } - } - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java b/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java deleted file mode 100644 index 8ecba36eb61f..000000000000 --- a/spring-core/src/main/java/org/springframework/util/concurrent/SuccessCallback.java +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.function.BiConsumer; - -import org.springframework.lang.Nullable; - -/** - * Success callback for a {@link ListenableFuture}. - * - * @author Sebastien Deleuze - * @since 4.1 - * @param the result type - * @deprecated as of 6.0, in favor of - * {@link java.util.concurrent.CompletableFuture#whenComplete(BiConsumer)} - */ -@Deprecated(since = "6.0", forRemoval = true) -@FunctionalInterface -public interface SuccessCallback { - - /** - * Called when the {@link ListenableFuture} completes with success. - *

    Note that Exceptions raised by this method are ignored. - * @param result the result - */ - void onSuccess(@Nullable T result); - -} diff --git a/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java b/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java index fc4777eff0f0..556f3300be26 100644 --- a/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/concurrent/package-info.java @@ -1,9 +1,7 @@ /** * Useful generic {@code java.util.concurrent.Future} extensions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util.concurrent; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java index 9cd7f7b33f52..9a7c59b15cb7 100644 --- a/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.java +++ b/spring-core/src/main/java/org/springframework/util/function/SingletonSupplier.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,7 +20,9 @@ import java.util.concurrent.locks.ReentrantLock; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; import org.springframework.util.Assert; /** @@ -37,16 +39,13 @@ * @since 5.1 * @param the type of results supplied by this supplier */ -public class SingletonSupplier implements Supplier { +public class SingletonSupplier implements Supplier<@Nullable T> { - @Nullable - private final Supplier instanceSupplier; + private final @Nullable Supplier instanceSupplier; - @Nullable - private final Supplier defaultSupplier; + private final @Nullable Supplier defaultSupplier; - @Nullable - private volatile T singletonInstance; + private volatile @Nullable T singletonInstance; /** * Guards access to write operations on the {@code singletonInstance} field. @@ -60,7 +59,7 @@ public class SingletonSupplier implements Supplier { * @param instance the singleton instance (potentially {@code null}) * @param defaultSupplier the default supplier as a fallback */ - public SingletonSupplier(@Nullable T instance, Supplier defaultSupplier) { + public SingletonSupplier(@Nullable T instance, Supplier defaultSupplier) { this.instanceSupplier = null; this.defaultSupplier = defaultSupplier; this.singletonInstance = instance; @@ -72,17 +71,17 @@ public SingletonSupplier(@Nullable T instance, Supplier defaultSupp * @param instanceSupplier the immediate instance supplier * @param defaultSupplier the default supplier as a fallback */ - public SingletonSupplier(@Nullable Supplier instanceSupplier, Supplier defaultSupplier) { + public SingletonSupplier(@Nullable Supplier instanceSupplier, Supplier defaultSupplier) { this.instanceSupplier = instanceSupplier; this.defaultSupplier = defaultSupplier; } - private SingletonSupplier(Supplier supplier) { + private SingletonSupplier(Supplier supplier) { this.instanceSupplier = supplier; this.defaultSupplier = null; } - private SingletonSupplier(T singletonInstance) { + private SingletonSupplier(@Nullable T singletonInstance) { this.instanceSupplier = null; this.defaultSupplier = null; this.singletonInstance = singletonInstance; @@ -94,8 +93,7 @@ private SingletonSupplier(T singletonInstance) { * @return the singleton instance (or {@code null} if none) */ @Override - @Nullable - public T get() { + public @Nullable T get() { T instance = this.singletonInstance; if (instance == null) { this.writeLock.lock(); @@ -144,8 +142,8 @@ public static SingletonSupplier of(T instance) { * @param instance the singleton instance (potentially {@code null}) * @return the singleton supplier, or {@code null} if the instance was {@code null} */ - @Nullable - public static SingletonSupplier ofNullable(@Nullable T instance) { + @Contract("null -> null; !null -> !null") + public static @Nullable SingletonSupplier ofNullable(@Nullable T instance) { return (instance != null ? new SingletonSupplier<>(instance) : null); } @@ -163,8 +161,8 @@ public static SingletonSupplier of(Supplier supplier) { * @param supplier the instance supplier (potentially {@code null}) * @return the singleton supplier, or {@code null} if the instance supplier was {@code null} */ - @Nullable - public static SingletonSupplier ofNullable(@Nullable Supplier supplier) { + @Contract("null -> null; !null -> !null") + public static @Nullable SingletonSupplier ofNullable(@Nullable Supplier<@Nullable T> supplier) { return (supplier != null ? new SingletonSupplier<>(supplier) : null); } 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 af97e2fd99e0..d593f8b40870 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 @@ -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,7 +18,9 @@ import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + +import org.springframework.lang.Contract; /** * Convenience utilities for {@link java.util.function.Supplier} handling. @@ -35,8 +37,8 @@ public abstract class SupplierUtils { * @param supplier the supplier to resolve * @return the supplier's result, or {@code null} if none */ - @Nullable - public static T resolve(@Nullable Supplier supplier) { + @Contract("null -> null") + public static @Nullable T resolve(@Nullable Supplier supplier) { return (supplier != null ? supplier.get() : null); } @@ -47,8 +49,8 @@ public static T resolve(@Nullable Supplier supplier) { * @return a supplier's result or the given Object as-is * @since 6.1.4 */ - @Nullable - public static Object resolve(@Nullable Object candidate) { + @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/java/org/springframework/util/function/package-info.java b/spring-core/src/main/java/org/springframework/util/function/package-info.java index 7c16649bd9a5..8ecebcf9a38b 100644 --- a/spring-core/src/main/java/org/springframework/util/function/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/function/package-info.java @@ -1,9 +1,7 @@ /** * Useful generic {@code java.util.function} helper classes. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util.function; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/util/package-info.java b/spring-core/src/main/java/org/springframework/util/package-info.java index 1c57d89eed18..55dca7cc132c 100644 --- a/spring-core/src/main/java/org/springframework/util/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/package-info.java @@ -2,9 +2,7 @@ * Miscellaneous utility classes, such as utilities for working with strings, * classes, collections, reflection, etc. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/util/unit/DataSize.java b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java index 33f76e9586ee..25c234c5d427 100644 --- a/spring-core/src/main/java/org/springframework/util/unit/DataSize.java +++ b/spring-core/src/main/java/org/springframework/util/unit/DataSize.java @@ -20,7 +20,8 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-core/src/main/java/org/springframework/util/unit/package-info.java b/spring-core/src/main/java/org/springframework/util/unit/package-info.java index bad1762bf616..91c7390e4faf 100644 --- a/spring-core/src/main/java/org/springframework/util/unit/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/unit/package-info.java @@ -1,9 +1,7 @@ /** * Useful unit data types. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util.unit; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java index 4a4c443e2134..e440f5e6a834 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxHandler.java @@ -25,13 +25,12 @@ import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamException; +import org.jspecify.annotations.Nullable; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.ext.LexicalHandler; -import org.springframework.lang.Nullable; - /** * Abstract base class for SAX {@code ContentHandler} and {@code LexicalHandler} * implementations that use StAX as a basis. All methods delegate to internal template diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java index 7d3e03b8d586..ef128895e112 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractStaxXMLReader.java @@ -23,6 +23,7 @@ import javax.xml.stream.Location; import javax.xml.stream.XMLStreamException; +import org.jspecify.annotations.Nullable; import org.xml.sax.InputSource; import org.xml.sax.Locator; import org.xml.sax.SAXException; @@ -30,7 +31,6 @@ import org.xml.sax.SAXNotSupportedException; import org.xml.sax.SAXParseException; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -57,8 +57,7 @@ abstract class AbstractStaxXMLReader extends AbstractXMLReader { private boolean namespacePrefixesFeature = false; - @Nullable - private Boolean isStandalone; + private @Nullable Boolean isStandalone; private final Map namespaces = new LinkedHashMap<>(); diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java index 3cf3108db6dd..08f597e30621 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLReader.java @@ -16,6 +16,7 @@ package org.springframework.util.xml; +import org.jspecify.annotations.Nullable; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; import org.xml.sax.EntityResolver; @@ -25,8 +26,6 @@ import org.xml.sax.XMLReader; import org.xml.sax.ext.LexicalHandler; -import org.springframework.lang.Nullable; - /** * Abstract base class for SAX {@code XMLReader} implementations. * Contains properties as defined in {@link XMLReader}, and does not recognize any features. @@ -41,20 +40,15 @@ */ abstract class AbstractXMLReader implements XMLReader { - @Nullable - private DTDHandler dtdHandler; + private @Nullable DTDHandler dtdHandler; - @Nullable - private ContentHandler contentHandler; + private @Nullable ContentHandler contentHandler; - @Nullable - private EntityResolver entityResolver; + private @Nullable EntityResolver entityResolver; - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; - @Nullable - private LexicalHandler lexicalHandler; + private @Nullable LexicalHandler lexicalHandler; @Override @@ -63,8 +57,7 @@ public void setContentHandler(@Nullable ContentHandler contentHandler) { } @Override - @Nullable - public ContentHandler getContentHandler() { + public @Nullable ContentHandler getContentHandler() { return this.contentHandler; } @@ -74,8 +67,7 @@ public void setDTDHandler(@Nullable DTDHandler dtdHandler) { } @Override - @Nullable - public DTDHandler getDTDHandler() { + public @Nullable DTDHandler getDTDHandler() { return this.dtdHandler; } @@ -85,8 +77,7 @@ public void setEntityResolver(@Nullable EntityResolver entityResolver) { } @Override - @Nullable - public EntityResolver getEntityResolver() { + public @Nullable EntityResolver getEntityResolver() { return this.entityResolver; } @@ -96,13 +87,11 @@ public void setErrorHandler(@Nullable ErrorHandler errorHandler) { } @Override - @Nullable - public ErrorHandler getErrorHandler() { + public @Nullable ErrorHandler getErrorHandler() { return this.errorHandler; } - @Nullable - protected LexicalHandler getLexicalHandler() { + protected @Nullable LexicalHandler getLexicalHandler() { return this.lexicalHandler; } @@ -144,8 +133,7 @@ public void setFeature(String name, boolean value) throws SAXNotRecognizedExcept * handler. The property name for a lexical handler is {@code http://xml.org/sax/properties/lexical-handler}. */ @Override - @Nullable - public Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { + public @Nullable Object getProperty(String name) throws SAXNotRecognizedException, SAXNotSupportedException { if ("http://xml.org/sax/properties/lexical-handler".equals(name)) { return this.lexicalHandler; } diff --git a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java index e50a5dd0dd48..3709dc8b920b 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/AbstractXMLStreamReader.java @@ -21,7 +21,7 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract base class for {@code XMLStreamReader}s. @@ -161,8 +161,7 @@ public void require(int expectedType, String namespaceURI, String localName) thr } @Override - @Nullable - public String getAttributeValue(@Nullable String namespaceURI, String localName) { + public @Nullable String getAttributeValue(@Nullable String namespaceURI, String localName) { for (int i = 0; i < getAttributeCount(); i++) { QName name = getAttributeName(i); if (name.getLocalPart().equals(localName) && diff --git a/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java b/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java index 6481dbba3091..9dc17011cca9 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java +++ b/spring-core/src/main/java/org/springframework/util/xml/DomUtils.java @@ -21,6 +21,7 @@ import java.util.Collection; import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.CharacterData; import org.w3c.dom.Comment; import org.w3c.dom.Element; @@ -29,7 +30,6 @@ import org.w3c.dom.NodeList; import org.xml.sax.ContentHandler; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -92,8 +92,7 @@ public static List getChildElementsByTagName(Element ele, String childE * @param childEleName the child element name to look for * @return the {@code org.w3c.dom.Element} instance, or {@code null} if none found */ - @Nullable - public static Element getChildElementByTagName(Element ele, String childEleName) { + public static @Nullable Element getChildElementByTagName(Element ele, String childEleName) { Assert.notNull(ele, "Element must not be null"); Assert.notNull(childEleName, "Element name must not be null"); NodeList nl = ele.getChildNodes(); @@ -112,8 +111,7 @@ public static Element getChildElementByTagName(Element ele, String childEleName) * @param childEleName the child element name to look for * @return the extracted text value, or {@code null} if no child element found */ - @Nullable - public static String getChildElementValueByTagName(Element ele, String childEleName) { + public static @Nullable String getChildElementValueByTagName(Element ele, String childEleName) { Element child = getChildElementByTagName(ele, childEleName); return (child != null ? getTextValue(child) : null); } diff --git a/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java b/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java index 488b70b47d91..b5708256ab9d 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/ListBasedXMLEventReader.java @@ -25,7 +25,8 @@ import javax.xml.stream.events.Characters; import javax.xml.stream.events.XMLEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -40,8 +41,7 @@ class ListBasedXMLEventReader extends AbstractXMLEventReader { private final List events; - @Nullable - private XMLEvent currentEvent; + private @Nullable XMLEvent currentEvent; private int cursor = 0; @@ -70,8 +70,7 @@ public XMLEvent nextEvent() { } @Override - @Nullable - public XMLEvent peek() { + public @Nullable XMLEvent peek() { if (hasNext()) { return this.events.get(this.cursor); } @@ -105,8 +104,7 @@ else if (!event.isCharacters()) { } @Override - @Nullable - public XMLEvent nextTag() throws XMLStreamException { + public @Nullable XMLEvent nextTag() throws XMLStreamException { checkIfClosed(); while (true) { diff --git a/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java index 4b8abba3903c..76e09ef3957b 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java +++ b/spring-core/src/main/java/org/springframework/util/xml/SimpleNamespaceContext.java @@ -26,7 +26,8 @@ import javax.xml.XMLConstants; import javax.xml.namespace.NamespaceContext; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -66,8 +67,7 @@ else if (this.prefixToNamespaceUri.containsKey(prefix)) { } @Override - @Nullable - public String getPrefix(String namespaceUri) { + public @Nullable String getPrefix(String namespaceUri) { Set prefixes = getPrefixesSet(namespaceUri); return (!prefixes.isEmpty() ? prefixes.iterator().next() : null); } diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java index e57141af1e2e..67281a9dc7ef 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventHandler.java @@ -28,12 +28,11 @@ import javax.xml.stream.events.Attribute; import javax.xml.stream.events.Namespace; +import org.jspecify.annotations.Nullable; import org.xml.sax.Attributes; import org.xml.sax.Locator; import org.xml.sax.ext.LexicalHandler; -import org.springframework.lang.Nullable; - /** * SAX {@link org.xml.sax.ContentHandler} and {@link LexicalHandler} * that writes to a {@link javax.xml.stream.util.XMLEventConsumer}. diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java index f5179707ca16..b40477896c77 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxEventXMLReader.java @@ -37,13 +37,13 @@ import javax.xml.stream.events.StartElement; import javax.xml.stream.events.XMLEvent; +import org.jspecify.annotations.Nullable; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.ext.Locator2; import org.xml.sax.helpers.AttributesImpl; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -67,8 +67,7 @@ class StaxEventXMLReader extends AbstractStaxXMLReader { private String xmlVersion = DEFAULT_XML_VERSION; - @Nullable - private String encoding; + private @Nullable String encoding; /** @@ -164,13 +163,11 @@ public int getLineNumber() { return (location != null ? location.getLineNumber() : -1); } @Override - @Nullable - public String getPublicId() { + public @Nullable String getPublicId() { return (location != null ? location.getPublicId() : null); } @Override - @Nullable - public String getSystemId() { + public @Nullable String getSystemId() { return (location != null ? location.getSystemId() : null); } @Override @@ -178,8 +175,7 @@ public String getXMLVersion() { return xmlVersion; } @Override - @Nullable - public String getEncoding() { + public @Nullable String getEncoding() { return encoding; } }); diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java b/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java index 80a86a04a312..767af71cf1a4 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxResult.java @@ -20,11 +20,10 @@ import javax.xml.stream.XMLStreamWriter; import javax.xml.transform.sax.SAXResult; +import org.jspecify.annotations.Nullable; import org.xml.sax.ContentHandler; import org.xml.sax.ext.LexicalHandler; -import org.springframework.lang.Nullable; - /** * Implementation of the {@code Result} tagging interface for StAX writers. Can be constructed with * an {@code XMLEventConsumer} or an {@code XMLStreamWriter}. @@ -48,11 +47,9 @@ */ class StaxResult extends SAXResult { - @Nullable - private XMLEventWriter eventWriter; + private @Nullable XMLEventWriter eventWriter; - @Nullable - private XMLStreamWriter streamWriter; + private @Nullable XMLStreamWriter streamWriter; /** @@ -85,8 +82,7 @@ public StaxResult(XMLStreamWriter streamWriter) { * @return the StAX event writer used by this result * @see #StaxResult(javax.xml.stream.XMLEventWriter) */ - @Nullable - public XMLEventWriter getXMLEventWriter() { + public @Nullable XMLEventWriter getXMLEventWriter() { return this.eventWriter; } @@ -97,8 +93,7 @@ public XMLEventWriter getXMLEventWriter() { * @return the StAX stream writer used by this result * @see #StaxResult(javax.xml.stream.XMLStreamWriter) */ - @Nullable - public XMLStreamWriter getXMLStreamWriter() { + public @Nullable XMLStreamWriter getXMLStreamWriter() { return this.streamWriter; } diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java b/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java index 392cd123c9fe..dcd0e82422a8 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxSource.java @@ -20,11 +20,10 @@ import javax.xml.stream.XMLStreamReader; import javax.xml.transform.sax.SAXSource; +import org.jspecify.annotations.Nullable; import org.xml.sax.InputSource; import org.xml.sax.XMLReader; -import org.springframework.lang.Nullable; - /** * Implementation of the {@code Source} tagging interface for StAX readers. Can be constructed with * an {@code XMLEventReader} or an {@code XMLStreamReader}. @@ -47,11 +46,9 @@ */ class StaxSource extends SAXSource { - @Nullable - private XMLEventReader eventReader; + private @Nullable XMLEventReader eventReader; - @Nullable - private XMLStreamReader streamReader; + private @Nullable XMLStreamReader streamReader; /** @@ -86,8 +83,7 @@ class StaxSource extends SAXSource { * @return the StAX event reader used by this source * @see StaxSource#StaxSource(javax.xml.stream.XMLEventReader) */ - @Nullable - XMLEventReader getXMLEventReader() { + @Nullable XMLEventReader getXMLEventReader() { return this.eventReader; } @@ -98,8 +94,7 @@ XMLEventReader getXMLEventReader() { * @return the StAX event reader used by this source * @see StaxSource#StaxSource(javax.xml.stream.XMLEventReader) */ - @Nullable - XMLStreamReader getXMLStreamReader() { + @Nullable XMLStreamReader getXMLStreamReader() { return this.streamReader; } diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java index ebc5386ff17b..55889f233021 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxStreamXMLReader.java @@ -22,13 +22,13 @@ import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; +import org.jspecify.annotations.Nullable; import org.xml.sax.Attributes; import org.xml.sax.ContentHandler; import org.xml.sax.SAXException; import org.xml.sax.ext.Locator2; import org.xml.sax.helpers.AttributesImpl; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -51,8 +51,7 @@ class StaxStreamXMLReader extends AbstractStaxXMLReader { private String xmlVersion = DEFAULT_XML_VERSION; - @Nullable - private String encoding; + private @Nullable String encoding; /** @@ -142,13 +141,11 @@ public int getLineNumber() { return (location != null ? location.getLineNumber() : -1); } @Override - @Nullable - public String getPublicId() { + public @Nullable String getPublicId() { return (location != null ? location.getPublicId() : null); } @Override - @Nullable - public String getSystemId() { + public @Nullable String getSystemId() { return (location != null ? location.getSystemId() : null); } @Override @@ -156,8 +153,7 @@ public String getXMLVersion() { return xmlVersion; } @Override - @Nullable - public String getEncoding() { + public @Nullable String getEncoding() { return encoding; } }); diff --git a/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java b/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java index 3555606de4bc..de4d578c2b30 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java +++ b/spring-core/src/main/java/org/springframework/util/xml/StaxUtils.java @@ -34,11 +34,10 @@ import javax.xml.transform.stax.StAXResult; import javax.xml.transform.stax.StAXSource; +import org.jspecify.annotations.Nullable; import org.xml.sax.ContentHandler; import org.xml.sax.XMLReader; -import org.springframework.lang.Nullable; - /** * Convenience methods for working with the StAX API. Partly historic due to JAXP 1.3 * compatibility; as of Spring 4.0, relying on JAXP 1.4 as included in JDK 1.6 and higher. @@ -134,8 +133,7 @@ public static boolean isStaxSource(Source source) { * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXSource} * or custom StAX Source */ - @Nullable - public static XMLStreamReader getXMLStreamReader(Source source) { + public static @Nullable XMLStreamReader getXMLStreamReader(Source source) { if (source instanceof StAXSource stAXSource) { return stAXSource.getXMLStreamReader(); } @@ -154,8 +152,7 @@ else if (source instanceof StaxSource staxSource) { * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXSource} * or custom StAX Source */ - @Nullable - public static XMLEventReader getXMLEventReader(Source source) { + public static @Nullable XMLEventReader getXMLEventReader(Source source) { if (source instanceof StAXSource stAXSource) { return stAXSource.getXMLEventReader(); } @@ -220,8 +217,7 @@ public static boolean isStaxResult(Result result) { * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXResult} * or custom StAX Result */ - @Nullable - public static XMLStreamWriter getXMLStreamWriter(Result result) { + public static @Nullable XMLStreamWriter getXMLStreamWriter(Result result) { if (result instanceof StAXResult stAXResult) { return stAXResult.getXMLStreamWriter(); } @@ -240,8 +236,7 @@ else if (result instanceof StaxResult staxResult) { * @throws IllegalArgumentException if {@code source} isn't a JAXP 1.4 {@link StAXResult} * or custom StAX Result */ - @Nullable - public static XMLEventWriter getXMLEventWriter(Result result) { + public static @Nullable XMLEventWriter getXMLEventWriter(Result result) { if (result instanceof StAXResult stAXResult) { return stAXResult.getXMLEventWriter(); } diff --git a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java index 67d37d5e5f7b..5fec714d6fa9 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java +++ b/spring-core/src/main/java/org/springframework/util/xml/XMLEventStreamReader.java @@ -31,7 +31,7 @@ import javax.xml.stream.events.StartDocument; import javax.xml.stream.events.XMLEvent; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Implementation of the {@link javax.xml.stream.XMLStreamReader} interface that wraps a @@ -79,8 +79,7 @@ public int getEventType() { } @Override - @Nullable - public String getVersion() { + public @Nullable String getVersion() { if (this.event.isStartDocument()) { return ((StartDocument) this.event).getVersion(); } @@ -115,14 +114,12 @@ public boolean standaloneSet() { } @Override - @Nullable - public String getEncoding() { + public @Nullable String getEncoding() { return null; } @Override - @Nullable - public String getCharacterEncodingScheme() { + public @Nullable String getCharacterEncodingScheme() { return null; } diff --git a/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java index 1248ab0b3948..06c242062f5d 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java +++ b/spring-core/src/main/java/org/springframework/util/xml/XmlValidationModeDetector.java @@ -22,7 +22,8 @@ import java.io.InputStream; import java.io.InputStreamReader; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.StringUtils; /** @@ -170,8 +171,7 @@ private String consumeCommentTokens(String line) { * Consume the next comment token, update the "inComment" flag, * and return the remaining content. */ - @Nullable - private String consume(String line) { + private @Nullable String consume(String line) { int index = (this.inComment ? endComment(line) : startComment(line)); return (index == -1 ? null : line.substring(index)); } diff --git a/spring-core/src/main/java/org/springframework/util/xml/package-info.java b/spring-core/src/main/java/org/springframework/util/xml/package-info.java index b077891dd66f..bc39307e9201 100644 --- a/spring-core/src/main/java/org/springframework/util/xml/package-info.java +++ b/spring-core/src/main/java/org/springframework/util/xml/package-info.java @@ -2,9 +2,7 @@ * Miscellaneous utility classes for XML parsing and transformation, * such as error handlers that log warnings via Commons Logging. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.util.xml; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 new file mode 100644 index 000000000000..a55fea416911 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileAnnotationMetadata.java @@ -0,0 +1,146 @@ +/* + * Copyright 2002-2025 the original author 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 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; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.AnnotationFilter; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.ClassUtils; + +/** + * Parse {@link RuntimeVisibleAnnotationsAttribute} into {@link MergedAnnotations} + * instances. + * @author Brian Clozel + */ +abstract class ClassFileAnnotationMetadata { + + static MergedAnnotations createMergedAnnotations(String className, RuntimeVisibleAnnotationsAttribute annotationAttribute, @Nullable ClassLoader classLoader) { + Set> annotations = annotationAttribute.annotations() + .stream() + .map(ann -> createMergedAnnotation(className, ann, classLoader)) + .filter(Objects::nonNull) + .collect(Collectors.toSet()); + return MergedAnnotations.of(annotations); + } + + @SuppressWarnings("unchecked") + private static @Nullable MergedAnnotation createMergedAnnotation(String className, Annotation annotation, @Nullable ClassLoader classLoader) { + String typeName = fromTypeDescriptor(annotation.className().stringValue()); + if (AnnotationFilter.PLAIN.matches(typeName)) { + return null; + } + Map attributes = new LinkedHashMap<>(4); + try { + for (AnnotationElement element : annotation.elements()) { + Object annotationValue = readAnnotationValue(className, element.value(), classLoader); + if (annotationValue != null) { + attributes.put(element.name().stringValue(), annotationValue); + } + } + Map compactedAttributes = (attributes.isEmpty() ? Collections.emptyMap() : attributes); + Class annotationType = (Class) ClassUtils.forName(typeName, classLoader); + return MergedAnnotation.of(classLoader, new Source(annotation), annotationType, compactedAttributes); + } + catch (ClassNotFoundException | LinkageError ex) { + return null; + } + } + + private static @Nullable Object readAnnotationValue(String className, AnnotationValue elementValue, @Nullable ClassLoader classLoader) { + switch (elementValue) { + case AnnotationValue.OfConstant constantValue -> { + return constantValue.resolvedValue(); + } + case AnnotationValue.OfAnnotation annotationValue -> { + return createMergedAnnotation(className, annotationValue.annotation(), classLoader); + } + case AnnotationValue.OfClass classValue -> { + return fromTypeDescriptor(classValue.className().stringValue()); + } + case AnnotationValue.OfEnum enumValue -> { + return parseEnum(enumValue, classLoader); + } + case AnnotationValue.OfArray arrayValue -> { + return parseArrayValue(className, classLoader, arrayValue); + } + } + } + + private static String fromTypeDescriptor(String descriptor) { + 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) { + if (arrayValue.values().isEmpty()) { + return new Object[0]; + } + Stream stream = arrayValue.values().stream(); + switch (arrayValue.values().getFirst()) { + case AnnotationValue.OfInt _ -> { + return stream.map(AnnotationValue.OfInt.class::cast).mapToInt(AnnotationValue.OfInt::intValue).toArray(); + } + case AnnotationValue.OfDouble _ -> { + return stream.map(AnnotationValue.OfDouble.class::cast).mapToDouble(AnnotationValue.OfDouble::doubleValue).toArray(); + } + case AnnotationValue.OfLong _ -> { + return stream.map(AnnotationValue.OfLong.class::cast).mapToLong(AnnotationValue.OfLong::longValue).toArray(); + } + default -> { + Object firstResolvedValue = readAnnotationValue(className, arrayValue.values().getFirst(), classLoader); + return stream + .map(rawValue -> readAnnotationValue(className, rawValue, classLoader)) + .toArray(s -> (Object[]) Array.newInstance(firstResolvedValue.getClass(), s)); + } + } + } + + @SuppressWarnings("unchecked") + private static @Nullable > Enum parseEnum(AnnotationValue.OfEnum enumValue, @Nullable ClassLoader classLoader) { + String enumClassName = fromTypeDescriptor(enumValue.className().stringValue()); + try { + Class enumClass = (Class) ClassUtils.forName(enumClassName, classLoader); + return Enum.valueOf(enumClass, enumValue.constantName().stringValue()); + } + catch (ClassNotFoundException | LinkageError ex) { + return null; + } + } + + record Source(Annotation entryName) { + + } + +} 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 new file mode 100644 index 000000000000..369511513bb1 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileClassMetadata.java @@ -0,0 +1,303 @@ +/* + * Copyright 2002-2025 the original author 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 java.lang.classfile.AccessFlags; +import java.lang.classfile.ClassModel; +import java.lang.classfile.Interfaces; +import java.lang.classfile.MethodModel; +import java.lang.classfile.Superclass; +import java.lang.classfile.attribute.InnerClassInfo; +import java.lang.classfile.attribute.InnerClassesAttribute; +import java.lang.classfile.attribute.NestHostAttribute; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.classfile.constantpool.ClassEntry; +import java.lang.reflect.AccessFlag; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.MethodMetadata; +import org.springframework.util.ClassUtils; +import org.springframework.util.StringUtils; + +/** + * {@link AnnotationMetadata} implementation that leverages + * the {@link java.lang.classfile.ClassFile} API. + * @author Brian Clozel + */ +class ClassFileClassMetadata implements AnnotationMetadata { + + private final String className; + + private final AccessFlags accessFlags; + + private final @Nullable String enclosingClassName; + + private final @Nullable String superClassName; + + private final boolean independentInnerClass; + + private final Set interfaceNames; + + private final Set memberClassNames; + + private final Set declaredMethods; + + private final MergedAnnotations mergedAnnotations; + + private @Nullable Set annotationTypes; + + ClassFileClassMetadata(String className, AccessFlags accessFlags, @Nullable String enclosingClassName, + @Nullable String superClassName, boolean independentInnerClass, Set interfaceNames, + Set memberClassNames, Set declaredMethods, MergedAnnotations mergedAnnotations) { + this.className = className; + this.accessFlags = accessFlags; + this.enclosingClassName = enclosingClassName; + this.superClassName = (!className.endsWith(".package-info")) ? superClassName : null; + this.independentInnerClass = independentInnerClass; + this.interfaceNames = interfaceNames; + this.memberClassNames = memberClassNames; + this.declaredMethods = declaredMethods; + this.mergedAnnotations = mergedAnnotations; + } + + @Override + public String getClassName() { + return this.className; + } + + @Override + public boolean isInterface() { + return this.accessFlags.has(AccessFlag.INTERFACE); + } + + @Override + public boolean isAnnotation() { + return this.accessFlags.has(AccessFlag.ANNOTATION); + } + + @Override + public boolean isAbstract() { + return this.accessFlags.has(AccessFlag.ABSTRACT); + } + + @Override + public boolean isFinal() { + return this.accessFlags.has(AccessFlag.FINAL); + } + + @Override + public boolean isIndependent() { + return (this.enclosingClassName == null || this.independentInnerClass); + } + + @Override + public @Nullable String getEnclosingClassName() { + return this.enclosingClassName; + } + + @Override + public @Nullable String getSuperClassName() { + return this.superClassName; + } + + @Override + public String[] getInterfaceNames() { + return StringUtils.toStringArray(this.interfaceNames); + } + + @Override + public String[] getMemberClassNames() { + return StringUtils.toStringArray(this.memberClassNames); + } + + @Override + public MergedAnnotations getAnnotations() { + return this.mergedAnnotations; + } + + @Override + public Set getAnnotationTypes() { + Set annotationTypes = this.annotationTypes; + if (annotationTypes == null) { + annotationTypes = Collections.unmodifiableSet( + AnnotationMetadata.super.getAnnotationTypes()); + this.annotationTypes = annotationTypes; + } + return annotationTypes; + } + + @Override + public Set getAnnotatedMethods(String annotationName) { + Set result = new LinkedHashSet<>(4); + for (MethodMetadata annotatedMethod : this.declaredMethods) { + if (annotatedMethod.isAnnotated(annotationName)) { + result.add(annotatedMethod); + } + } + return Collections.unmodifiableSet(result); + } + + @Override + public Set getDeclaredMethods() { + return Collections.unmodifiableSet(this.declaredMethods); + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ClassFileClassMetadata that && this.className.equals(that.className))); + } + + @Override + public int hashCode() { + return this.className.hashCode(); + } + + @Override + public String toString() { + return this.className; + } + + + static ClassFileClassMetadata of(ClassModel classModel, @Nullable ClassLoader classLoader) { + Builder builder = new Builder(classLoader); + builder.classEntry(classModel.thisClass()); + String currentClassName = classModel.thisClass().name().stringValue(); + classModel.elementStream().forEach(classElement -> { + switch (classElement) { + case AccessFlags flags -> { + builder.accessFlags(flags); + } + case NestHostAttribute _ -> { + builder.enclosingClass(classModel.thisClass()); + } + case InnerClassesAttribute innerClasses -> { + builder.nestMembers(currentClassName, innerClasses); + } + case RuntimeVisibleAnnotationsAttribute annotationsAttribute -> { + builder.mergedAnnotations(ClassFileAnnotationMetadata.createMergedAnnotations( + ClassUtils.convertResourcePathToClassName(currentClassName), annotationsAttribute, classLoader)); + } + case Superclass superclass -> { + builder.superClass(superclass); + } + case Interfaces interfaces -> { + builder.interfaces(interfaces); + } + case MethodModel method -> { + builder.method(method); + } + default -> { + // ignore class element + } + } + }); + return builder.build(); + } + + static class Builder { + + private final ClassLoader clasLoader; + + private String className; + + private AccessFlags accessFlags; + + private Set innerAccessFlags; + + private @Nullable String enclosingClassName; + + private @Nullable String superClassName; + + private Set interfaceNames = new LinkedHashSet<>(4); + + private Set memberClassNames = new LinkedHashSet<>(4); + + private Set declaredMethods = new LinkedHashSet<>(4); + + private MergedAnnotations mergedAnnotations = MergedAnnotations.of(Collections.emptySet()); + + public Builder(ClassLoader classLoader) { + this.clasLoader = classLoader; + } + + void classEntry(ClassEntry classEntry) { + this.className = ClassUtils.convertResourcePathToClassName(classEntry.name().stringValue()); + } + + void accessFlags(AccessFlags accessFlags) { + this.accessFlags = accessFlags; + } + + void enclosingClass(ClassEntry thisClass) { + String thisClassName = thisClass.name().stringValue(); + int currentClassIndex = thisClassName.lastIndexOf('$'); + this.enclosingClassName = ClassUtils.convertResourcePathToClassName(thisClassName.substring(0, currentClassIndex)); + } + + void superClass(Superclass superClass) { + this.superClassName = ClassUtils.convertResourcePathToClassName(superClass.superclassEntry().name().stringValue()); + } + + void interfaces(Interfaces interfaces) { + for (ClassEntry entry : interfaces.interfaces()) { + this.interfaceNames.add(ClassUtils.convertResourcePathToClassName(entry.name().stringValue())); + } + } + + void nestMembers(String currentClassName, InnerClassesAttribute innerClasses) { + for (InnerClassInfo classInfo : innerClasses.classes()) { + String innerClassName = classInfo.innerClass().name().stringValue(); + if (currentClassName.equals(innerClassName)) { + // the current class is an inner class + this.innerAccessFlags = classInfo.flags(); + } + classInfo.outerClass().ifPresent(outerClass -> { + if (outerClass.name().stringValue().equals(currentClassName)) { + // collecting data about actual inner classes + this.memberClassNames.add(ClassUtils.convertResourcePathToClassName(innerClassName)); + } + }); + } + } + + void mergedAnnotations(MergedAnnotations mergedAnnotations) { + this.mergedAnnotations = mergedAnnotations; + } + + void method(MethodModel method) { + ClassFileMethodMetadata classFileMethodMetadata = ClassFileMethodMetadata.of(method, this.clasLoader); + if (!classFileMethodMetadata.isSynthetic() && !classFileMethodMetadata.isDefaultConstructor()) { + this.declaredMethods.add(classFileMethodMetadata); + } + } + + ClassFileClassMetadata build() { + boolean independentInnerClass = (this.enclosingClassName != null) && this.innerAccessFlags.contains(AccessFlag.STATIC); + return new ClassFileClassMetadata(this.className, this.accessFlags, this.enclosingClassName, this.superClassName, + independentInnerClass, this.interfaceNames, this.memberClassNames, this.declaredMethods, this.mergedAnnotations); + } + + } + +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.java new file mode 100644 index 000000000000..aadc3b012085 --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReader.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.core.type.classreading; + +import java.io.IOException; +import java.io.InputStream; +import java.lang.classfile.ClassFile; +import java.lang.classfile.ClassModel; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.core.type.ClassMetadata; + +/** + * {@link MetadataReader} implementation based on the {@link ClassFile} API. + * + * @author Brian Clozel + */ +final class ClassFileMetadataReader implements MetadataReader { + + private final Resource resource; + + private final AnnotationMetadata annotationMetadata; + + + ClassFileMetadataReader(Resource resource, @Nullable ClassLoader classLoader) throws IOException { + this.resource = resource; + this.annotationMetadata = ClassFileClassMetadata.of(parseClassModel(resource), classLoader); + } + + private static ClassModel parseClassModel(Resource resource) throws IOException { + try (InputStream is = resource.getInputStream()) { + byte[] bytes = is.readAllBytes(); + return ClassFile.of().parse(bytes); + } + } + + @Override + public Resource getResource() { + return this.resource; + } + + @Override + public ClassMetadata getClassMetadata() { + return this.annotationMetadata; + } + + @Override + public AnnotationMetadata getAnnotationMetadata() { + return this.annotationMetadata; + } + +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java new file mode 100644 index 000000000000..c267f3b0cfdb --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMetadataReaderFactory.java @@ -0,0 +1,105 @@ +/* + * Copyright 2002-2025 the original author 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 java.io.FileNotFoundException; +import java.io.IOException; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.io.DefaultResourceLoader; +import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; +import org.springframework.util.ClassUtils; + +/** + * Implementation of the {@link MetadataReaderFactory} interface, + * using the {@link java.lang.classfile.ClassFile} API for parsing the bytecode. + * + * @author Brian Clozel + * @since 7.0 + */ +public class ClassFileMetadataReaderFactory implements MetadataReaderFactory { + + + private final ResourceLoader resourceLoader; + + + /** + * Create a new ClassFileMetadataReaderFactory for the default class loader. + */ + public ClassFileMetadataReaderFactory() { + this.resourceLoader = new DefaultResourceLoader(); + } + + /** + * Create a new ClassFileMetadataReaderFactory for the given resource loader. + * @param resourceLoader the Spring ResourceLoader to use + * (also determines the ClassLoader to use) + */ + public ClassFileMetadataReaderFactory(@Nullable ResourceLoader resourceLoader) { + this.resourceLoader = (resourceLoader != null ? resourceLoader : new DefaultResourceLoader()); + } + + /** + * Create a new ClassFileMetadataReaderFactory for the given class loader. + * @param classLoader the ClassLoader to use + */ + public ClassFileMetadataReaderFactory(@Nullable ClassLoader classLoader) { + this.resourceLoader = + (classLoader != null ? new DefaultResourceLoader(classLoader) : new DefaultResourceLoader()); + } + + /** + * Return the ResourceLoader that this MetadataReaderFactory has been + * constructed with. + */ + public final ResourceLoader getResourceLoader() { + return this.resourceLoader; + } + + @Override + public MetadataReader getMetadataReader(String className) throws IOException { + try { + String resourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(className) + ClassUtils.CLASS_FILE_SUFFIX; + Resource resource = this.resourceLoader.getResource(resourcePath); + return getMetadataReader(resource); + } + catch (FileNotFoundException ex) { + // Maybe an inner class name using the dot name syntax? Need to use the dollar syntax here... + // ClassUtils.forName has an equivalent check for resolution into Class references later on. + int lastDotIndex = className.lastIndexOf('.'); + if (lastDotIndex != -1) { + String innerClassName = + className.substring(0, lastDotIndex) + '$' + className.substring(lastDotIndex + 1); + String innerClassResourcePath = ResourceLoader.CLASSPATH_URL_PREFIX + + ClassUtils.convertClassNameToResourcePath(innerClassName) + ClassUtils.CLASS_FILE_SUFFIX; + Resource innerClassResource = this.resourceLoader.getResource(innerClassResourcePath); + if (innerClassResource.exists()) { + return getMetadataReader(innerClassResource); + } + } + throw ex; + } + } + + @Override + public MetadataReader getMetadataReader(Resource resource) throws IOException { + return new ClassFileMetadataReader(resource, this.resourceLoader.getClassLoader()); + } +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java new file mode 100644 index 000000000000..579f9bda917f --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/ClassFileMethodMetadata.java @@ -0,0 +1,182 @@ +/* + * Copyright 2002-2025 the original author 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 java.lang.classfile.AccessFlags; +import java.lang.classfile.MethodModel; +import java.lang.classfile.attribute.RuntimeVisibleAnnotationsAttribute; +import java.lang.constant.ClassDesc; +import java.lang.constant.MethodTypeDesc; +import java.lang.reflect.AccessFlag; +import java.util.Collections; +import java.util.Locale; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.jspecify.annotations.Nullable; + +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.core.type.MethodMetadata; +import org.springframework.util.ClassUtils; + +/** + * {@link MethodMetadata} extracted from class bytecode using the + * {@link java.lang.classfile.ClassFile} API. + * @author Brian Clozel + */ +class ClassFileMethodMetadata implements MethodMetadata { + + private final String methodName; + + private final AccessFlags accessFlags; + + private final @Nullable String declaringClassName; + + private final String returnTypeName; + + // The source implements equals(), hashCode(), and toString() for the underlying method. + private final Object source; + + private final MergedAnnotations annotations; + + ClassFileMethodMetadata(String methodName, AccessFlags accessFlags, @Nullable String declaringClassName, String returnTypeName, Object source, MergedAnnotations annotations) { + this.methodName = methodName; + this.accessFlags = accessFlags; + this.declaringClassName = declaringClassName; + this.returnTypeName = returnTypeName; + this.source = source; + this.annotations = annotations; + } + + @Override + public String getMethodName() { + return this.methodName; + } + + @Override + public @Nullable String getDeclaringClassName() { + return this.declaringClassName; + } + + @Override + public String getReturnTypeName() { + return this.returnTypeName; + } + + @Override + public boolean isAbstract() { + return this.accessFlags.has(AccessFlag.ABSTRACT); + } + + @Override + public boolean isStatic() { + return this.accessFlags.has(AccessFlag.STATIC); + } + + @Override + public boolean isFinal() { + return this.accessFlags.has(AccessFlag.FINAL); + } + + @Override + public boolean isOverridable() { + return !isStatic() && !isFinal() && !isPrivate(); + } + + private boolean isPrivate() { + return this.accessFlags.has(AccessFlag.PRIVATE); + } + + public boolean isSynthetic() { + return this.accessFlags.has(AccessFlag.SYNTHETIC); + } + + public boolean isDefaultConstructor() { + return this.methodName.equals(""); + } + + @Override + public MergedAnnotations getAnnotations() { + return this.annotations; + } + + + @Override + public boolean equals(@Nullable Object other) { + return (this == other || (other instanceof ClassFileMethodMetadata that && this.source.equals(that.source))); + } + + @Override + public int hashCode() { + return this.source.hashCode(); + } + + @Override + public String toString() { + return this.source.toString(); + } + + static ClassFileMethodMetadata of(MethodModel methodModel, ClassLoader classLoader) { + String methodName = methodModel.methodName().stringValue(); + AccessFlags flags = methodModel.flags(); + String declaringClassName = methodModel.parent().map(parent -> ClassUtils.convertResourcePathToClassName(parent.thisClass().name().stringValue())).orElse(null); + ClassDesc returnType = methodModel.methodTypeSymbol().returnType(); + String returnTypeName = returnType.packageName() + "." + returnType.displayName(); + Source source = new Source(declaringClassName, flags, methodName, methodModel.methodTypeSymbol()); + MergedAnnotations annotations = methodModel.elementStream() + .filter(element -> element instanceof RuntimeVisibleAnnotationsAttribute) + .findFirst() + .map(element -> ClassFileAnnotationMetadata.createMergedAnnotations(methodName, (RuntimeVisibleAnnotationsAttribute) element, classLoader)) + .orElse(MergedAnnotations.of(Collections.emptyList())); + return new ClassFileMethodMetadata(methodName, flags, declaringClassName, returnTypeName, source, annotations); + } + + /** + * {@link MergedAnnotation} source. + * + * @param declaringClassName the name of the declaring class + * @param flags the access flags + * @param methodName the name of the method + * @param descriptor the bytecode descriptor for this method + */ + record Source(@Nullable String declaringClassName, AccessFlags flags, String methodName, MethodTypeDesc descriptor) { + + @Override + public String toString() { + StringBuilder builder = new StringBuilder(); + this.flags.flags().forEach(flag -> { + builder.append(flag.name().toLowerCase(Locale.ROOT)); + builder.append(' '); + }); + builder.append(this.descriptor.returnType().packageName()); + builder.append("."); + builder.append(this.descriptor.returnType().displayName()); + builder.append(' '); + builder.append(this.declaringClassName); + builder.append('.'); + builder.append(this.methodName); + builder.append('('); + builder.append(Stream.of(this.descriptor.parameterArray()) + .map(desc -> desc.packageName() + "." + desc.displayName()) + .collect(Collectors.joining(","))); + builder.append(')'); + return builder.toString(); + } + } + +} diff --git a/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java b/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java new file mode 100644 index 000000000000..8887ada0637d --- /dev/null +++ b/spring-core/src/main/java24/org/springframework/core/type/classreading/MetadataReaderFactoryDelegate.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2025 the original author 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.jspecify.annotations.Nullable; + +import org.springframework.core.io.ResourceLoader; + +/** + * Internal delegate for instantiating {@link MetadataReaderFactory} implementations. + * For JDK >= 24, the {@link ClassFileMetadataReaderFactory} is being used. + * + * @author Brian Clozel + * @see MetadataReaderFactory + */ +abstract class MetadataReaderFactoryDelegate { + + static MetadataReaderFactory create(@Nullable ResourceLoader resourceLoader) { + return new ClassFileMetadataReaderFactory(resourceLoader); + } + + static MetadataReaderFactory create(@Nullable ClassLoader classLoader) { + return new ClassFileMetadataReaderFactory(classLoader); + } +} diff --git a/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java b/spring-core/src/test/java/org/springframework/SpringCoreTestSuite.java index a2d623435387..33207e5c1c2d 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 @@ * @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/aot/generate/DefaultMethodReferenceTests.java b/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java index bc62da2b9ba4..083e0583dd9e 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/DefaultMethodReferenceTests.java @@ -18,6 +18,7 @@ import javax.lang.model.element.Modifier; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.MethodReference.ArgumentCodeGenerator; @@ -26,7 +27,6 @@ import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.MethodSpec.Builder; import org.springframework.javapoet.TypeName; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java index 5c45bca59941..55f5a71de2b5 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/GeneratedFilesTests.java @@ -24,6 +24,7 @@ import javax.lang.model.element.Modifier; import org.assertj.core.api.AbstractStringAssert; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aot.generate.GeneratedFiles.FileHandler; @@ -34,7 +35,6 @@ import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.MethodSpec; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import org.springframework.util.function.ThrowingConsumer; import static org.assertj.core.api.Assertions.assertThat; @@ -71,8 +71,8 @@ void addSourceFileWithJavaFileInTheDefaultPackageThrowsException() { TypeSpec helloWorld = TypeSpec.classBuilder("HelloWorld").build(); JavaFile javaFile = JavaFile.builder("", helloWorld).build(); assertThatIllegalArgumentException().isThrownBy(() -> this.generatedFiles.addSourceFile(javaFile)) - .withMessage("Could not add 'HelloWorld', processing classes in the " - + "default package is not supported. Did you forget to add a package statement?"); + .withMessage("Could not add 'HelloWorld', processing classes in the " + + "default package is not supported. Did you forget to add a package statement?"); } @Test @@ -92,8 +92,8 @@ void addSourceFileWithCharSequenceWhenClassNameIsEmptyThrowsException() { void addSourceFileWithCharSequenceWhenClassNameIsInTheDefaultPackageThrowsException() { assertThatIllegalArgumentException() .isThrownBy(() -> this.generatedFiles.addSourceFile("HelloWorld", "{}")) - .withMessage("Could not add 'HelloWorld', processing classes in the " - + "default package is not supported. Did you forget to add a package statement?"); + .withMessage("Could not add 'HelloWorld', processing classes in the " + + "default package is not supported. Did you forget to add a package statement?"); } @Test @@ -226,11 +226,9 @@ private GeneratedFileAssert assertThatFileAdded(Kind kind, String path) static class TestGeneratedFiles implements GeneratedFiles { - @Nullable - private Kind kind; + private @Nullable Kind kind; - @Nullable - private String path; + private @Nullable String path; private TestFileHandler fileHandler = new TestFileHandler(); @@ -256,8 +254,7 @@ GeneratedFileAssert assertThatFileAdded(Kind kind, String path) private static class GeneratedFileAssert extends AbstractStringAssert { - @Nullable - private final Boolean override; + private final @Nullable Boolean override; GeneratedFileAssert(InputStreamSource content, @Nullable Boolean override) throws IOException { super(readSource(content), GeneratedFileAssert.class); @@ -272,11 +269,9 @@ public GeneratedFileAssert hasOverride(boolean expected) { private static class TestFileHandler extends FileHandler { - @Nullable - private InputStreamSource content; + private @Nullable InputStreamSource content; - @Nullable - private Boolean override; + private @Nullable Boolean override; TestFileHandler(@Nullable InputStreamSource content) { super(content != null, () -> content); diff --git a/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java index dc755467ec1c..27dee0df145c 100644 --- a/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/generate/ValueCodeGeneratorTests.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,6 +33,7 @@ import org.assertj.core.api.AbstractAssert; import org.assertj.core.api.AssertProvider; import org.assertj.core.api.StringAssert; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -47,7 +48,6 @@ import org.springframework.javapoet.FieldSpec; import org.springframework.javapoet.JavaFile; import org.springframework.javapoet.TypeSpec; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -64,20 +64,17 @@ */ class ValueCodeGeneratorTests { - @Nested class ConfigurationTests { @Test void createWithListOfDelegatesInvokeThemInOrder() { - Delegate first = mock(Delegate.class); - Delegate second = mock(Delegate.class); - Delegate third = mock(Delegate.class); - ValueCodeGenerator codeGenerator = ValueCodeGenerator - .with(List.of(first, second, third)); + Delegate first = mock(); + Delegate second = mock(); + Delegate third = mock(); + ValueCodeGenerator codeGenerator = ValueCodeGenerator.with(List.of(first, second, third)); Object value = ""; - given(third.generateCode(codeGenerator, value)) - .willReturn(CodeBlock.of("test")); + given(third.generateCode(codeGenerator, value)).willReturn(CodeBlock.of("test")); CodeBlock code = codeGenerator.generateCode(value); assertThat(code).hasToString("test"); InOrder ordered = inOrder(first, second, third); @@ -88,13 +85,11 @@ void createWithListOfDelegatesInvokeThemInOrder() { @Test void generateCodeWithMatchingDelegateStops() { - Delegate first = mock(Delegate.class); - Delegate second = mock(Delegate.class); - ValueCodeGenerator codeGenerator = ValueCodeGenerator - .with(List.of(first, second)); + Delegate first = mock(); + Delegate second = mock(); + ValueCodeGenerator codeGenerator = ValueCodeGenerator.with(List.of(first, second)); Object value = ""; - given(first.generateCode(codeGenerator, value)) - .willReturn(CodeBlock.of("test")); + given(first.generateCode(codeGenerator, value)).willReturn(CodeBlock.of("test")); CodeBlock code = codeGenerator.generateCode(value); assertThat(code).hasToString("test"); verify(first).generateCode(codeGenerator, value); @@ -198,7 +193,6 @@ void generateWhenString() { assertThat(generateCode("test")).hasToString("\"test\""); } - @Test void generateWhenStringWithCarriageReturn() { assertThat(generateCode("test\n")).isEqualTo(CodeBlock.of("$S", "test\n")); @@ -285,9 +279,9 @@ void generateWhenNestedGenericResolvableType() { ResolvableType resolvableType = ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(Integer.class), stringList); assertThat(resolve(generateCode(resolvableType))) - .hasImport(ResolvableType.class, List.class, Map.class).hasValueCode( - "ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(Integer.class), " - + "ResolvableType.forClassWithGenerics(List.class, String.class))"); + .hasImport(ResolvableType.class, List.class, Map.class).hasValueCode(""" + ResolvableType.forClassWithGenerics(Map.class, ResolvableType.forClass(Integer.class), \ + ResolvableType.forClassWithGenerics(List.class, String.class))"""); } @Test diff --git a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java index 270b1e9b042b..55a3ccda8160 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/BindingReflectionHintsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ void registerTypeForSerializationWithEmptyClass() { .satisfies(typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleEmptyClass.class)); assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder( - MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); assertThat(typeHint.constructors()).isEmpty(); assertThat(typeHint.fields()).isEmpty(); assertThat(typeHint.methods()).isEmpty(); @@ -68,7 +68,7 @@ void registerTypeForSerializationWithExtendingClass() { typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleEmptyClass.class)); assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder( - MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); assertThat(typeHint.constructors()).isEmpty(); assertThat(typeHint.fields()).isEmpty(); assertThat(typeHint.methods()).isEmpty(); @@ -76,7 +76,7 @@ void registerTypeForSerializationWithExtendingClass() { typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(SampleExtendingClass.class)); assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder( - MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); assertThat(typeHint.constructors()).isEmpty(); assertThat(typeHint.fields()).isEmpty(); assertThat(typeHint.methods()).isEmpty(); @@ -198,7 +198,7 @@ void registerTypeForSerializationWithResolvableType() { typeHint -> { assertThat(typeHint.getType()).isEqualTo(TypeReference.of(ResolvableType.class)); assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder( - MemberCategory.DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); assertThat(typeHint.constructors()).isEmpty(); assertThat(typeHint.fields()).isEmpty(); assertThat(typeHint.methods()).hasSizeGreaterThan(1); @@ -254,7 +254,7 @@ void registerTypeForSerializationWithRecord() { @Test void registerTypeForSerializationWithRecordWithProperty() { bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleRecordWithProperty.class); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleRecordWithProperty.class, "getNameProperty")) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleRecordWithProperty.class, "getNameProperty")) .accepts(this.hints); } @@ -267,18 +267,18 @@ void registerTypeForSerializationWithAnonymousClass() { @Test void registerTypeForJacksonAnnotations() { bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleClassWithJsonProperty.class); - assertThat(RuntimeHintsPredicates.reflection().onField(SampleClassWithJsonProperty.class, "privateField")) + assertThat(RuntimeHintsPredicates.reflection().onFieldAccess(SampleClassWithJsonProperty.class, "privateField")) .accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleClassWithJsonProperty.class, "packagePrivateMethod").invoke()) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleClassWithJsonProperty.class, "packagePrivateMethod")) .accepts(this.hints); } @Test void registerTypeForInheritedJacksonAnnotations() { bindingRegistrar.registerReflectionHints(this.hints.reflection(), SampleClassWithInheritedJsonProperty.class); - assertThat(RuntimeHintsPredicates.reflection().onField(SampleClassWithJsonProperty.class, "privateField")) + assertThat(RuntimeHintsPredicates.reflection().onFieldAccess(SampleClassWithJsonProperty.class, "privateField")) .accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleClassWithJsonProperty.class, "packagePrivateMethod").invoke()) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleClassWithJsonProperty.class, "packagePrivateMethod")) .accepts(this.hints); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ExecutableHintTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ExecutableHintTests.java index f817ba56b82c..978934af799b 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ExecutableHintTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ExecutableHintTests.java @@ -28,6 +28,7 @@ * @author Phillip Webb * @since 6.0 */ +@SuppressWarnings("removal") class ExecutableHintTests { @Test diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java index d19df791de25..ec16727bb9c3 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ExecutableModeTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -25,6 +25,7 @@ * * @author Stephane Nicoll */ +@SuppressWarnings("removal") class ExecutableModeTests { @Test diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java index 5e4c3cc14f8a..48aea70fe9ca 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ReflectionHintsTests.java @@ -22,9 +22,9 @@ import java.lang.reflect.Method; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -37,6 +37,7 @@ * @author Stephane Nicoll * @author Sebastien Deleuze */ +@SuppressWarnings("removal") class ReflectionHintsTests { private final ReflectionHints reflectionHints = new ReflectionHints(); @@ -216,10 +217,10 @@ void registerOnInterfaces() { typeHint -> typeHint.withMembers(MemberCategory.INTROSPECT_PUBLIC_METHODS)); assertThat(this.reflectionHints.typeHints()).hasSize(2) .noneMatch(typeHint -> typeHint.getType().getCanonicalName().equals(Serializable.class.getCanonicalName())) - .anyMatch(typeHint -> typeHint.getType().getCanonicalName().equals(SecondInterface.class.getCanonicalName()) - && typeHint.getMemberCategories().contains(MemberCategory.INTROSPECT_PUBLIC_METHODS)) - .anyMatch(typeHint -> typeHint.getType().getCanonicalName().equals(FirstInterface.class.getCanonicalName()) - && typeHint.getMemberCategories().contains(MemberCategory.INTROSPECT_PUBLIC_METHODS)); + .anyMatch(typeHint -> typeHint.getType().getCanonicalName().equals(SecondInterface.class.getCanonicalName()) && + typeHint.getMemberCategories().contains(MemberCategory.INTROSPECT_PUBLIC_METHODS)) + .anyMatch(typeHint -> typeHint.getType().getCanonicalName().equals(FirstInterface.class.getCanonicalName()) && + typeHint.getMemberCategories().contains(MemberCategory.INTROSPECT_PUBLIC_METHODS)); } private void assertTestTypeMethodHints(Consumer methodHint) { @@ -245,8 +246,7 @@ private Consumer typeWithMemberCategories(Class type, MemberCategor @SuppressWarnings("unused") static class TestType { - @Nullable - private String field; + private @Nullable String field; void setName(String name) { diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java index b8393dd52ba3..19e57e873b7b 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ResourceHintsTests.java @@ -117,7 +117,7 @@ void registerPattern() { @Test void registerPatternWithIncludesAndExcludes() { this.resourceHints.registerPattern(resourceHint -> - resourceHint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties")); + resourceHint.includes("com/example/*.properties")); assertThat(this.resourceHints.resourcePatternHints()).singleElement().satisfies(patternOf( List.of("/", "com", "com/example", "com/example/*.properties"), List.of("com/example/to-ignore.properties"))); @@ -198,10 +198,7 @@ private Consumer resourceBundle(String baseName) { } private Consumer patternOf(List includes, List excludes) { - return pattern -> { - assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyInAnyOrderElementsOf(includes); - assertThat(pattern.getExcludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(excludes); - }; + return pattern -> assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyInAnyOrderElementsOf(includes); } static class Nested { diff --git a/spring-core/src/test/java/org/springframework/aot/hint/ResourcePatternHintTests.java b/spring-core/src/test/java/org/springframework/aot/hint/ResourcePatternHintTests.java index 303b243b2b7c..ecea7ee91574 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/ResourcePatternHintTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/ResourcePatternHintTests.java @@ -39,49 +39,55 @@ void patternWithLeadingSlashIsRejected() { @Test void rootDirectory() { ResourcePatternHint hint = new ResourcePatternHint("/", null); - assertThat(hint.toRegex().asMatchPredicate()) - .accepts("/") - .rejects("/com/example", "/file.txt"); + assertThat(hint.matches("/")).isTrue(); + assertThat(hint.matches("/com/example")).isFalse(); + assertThat(hint.matches("/file.txt")).isFalse(); } @Test void fileAtRoot() { ResourcePatternHint hint = new ResourcePatternHint("file.properties", null); - assertThat(hint.toRegex().asMatchPredicate()) - .accepts("file.properties") - .rejects("com/example/file.properties", "file.prop", "another-file.properties"); + assertThat(hint.matches("file.properties")).isTrue(); + assertThat(hint.matches("com/example/file.properties")).isFalse(); + assertThat(hint.matches("file.prop")).isFalse(); + assertThat(hint.matches("another-file.properties")).isFalse(); } @Test void fileInDirectory() { ResourcePatternHint hint = new ResourcePatternHint("com/example/file.properties", null); - assertThat(hint.toRegex().asMatchPredicate()) - .accepts("com/example/file.properties") - .rejects("file.properties", "com/file.properties", "com/example/another-file.properties"); + assertThat(hint.matches("com/example/file.properties")).isTrue(); + assertThat(hint.matches("file.properties")).isFalse(); + assertThat(hint.matches("com/file.properties")).isFalse(); + assertThat(hint.matches("com/example/another-file.properties")).isFalse(); } @Test void extension() { - ResourcePatternHint hint = new ResourcePatternHint("*.properties", null); - assertThat(hint.toRegex().asMatchPredicate()) - .accepts("file.properties", "com/example/file.properties") - .rejects("file.prop", "com/example/file.prop"); + ResourcePatternHint hint = new ResourcePatternHint("**/*.properties", null); + assertThat(hint.matches("file.properties")).isTrue(); + assertThat(hint.matches("com/example/file.properties")).isTrue(); + assertThat(hint.matches("file.prop")).isFalse(); + assertThat(hint.matches("com/example/file.prop")).isFalse(); } @Test void extensionInDirectoryAtAnyDepth() { ResourcePatternHint hint = new ResourcePatternHint("com/example/*.properties", null); - assertThat(hint.toRegex().asMatchPredicate()) - .accepts("com/example/file.properties", "com/example/another/file.properties") - .rejects("file.properties", "com/file.properties"); + assertThat(hint.matches("com/example/file.properties")).isTrue(); + assertThat(hint.matches("com/example/another/file.properties")).isFalse(); + assertThat(hint.matches("com/file.properties")).isFalse(); + assertThat(hint.matches("file.properties")).isFalse(); } @Test void anyFileInDirectoryAtAnyDepth() { - ResourcePatternHint hint = new ResourcePatternHint("com/example/*", null); - assertThat(hint.toRegex().asMatchPredicate()) - .accepts("com/example/file.properties", "com/example/another/file.properties", "com/example/another") - .rejects("file.properties", "com/file.properties"); + ResourcePatternHint hint = new ResourcePatternHint("com/example/**", null); + assertThat(hint.matches("com/example/file.properties")).isTrue(); + assertThat(hint.matches("com/example/another/file.properties")).isTrue(); + assertThat(hint.matches("com/example/another")).isTrue(); + assertThat(hint.matches("file.properties")).isFalse(); + assertThat(hint.matches("com/file.properties")).isFalse(); } } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java index f88170d28708..1778a82f0398 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/RuntimeHintsTests.java @@ -47,11 +47,9 @@ void reflectionHintWithClass() { @Test void resourceHintWithClass() { this.hints.resources().registerType(String.class); - assertThat(this.hints.resources().resourcePatternHints()).singleElement().satisfies(resourceHint -> { - assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern) - .containsExactlyInAnyOrder("/", "java", "java/lang", "java/lang/String.class"); - assertThat(resourceHint.getExcludes()).isEmpty(); - }); + assertThat(this.hints.resources().resourcePatternHints()).singleElement().satisfies(resourceHint -> + assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern) + .containsExactlyInAnyOrder("/", "java", "java/lang", "java/lang/String.class")); } @Test diff --git a/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java b/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java index 5c84ec93bbc8..4aa0f6b97a04 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/TypeHintTests.java @@ -31,6 +31,7 @@ * * @author Stephane Nicoll */ +@SuppressWarnings("removal") class TypeHintTests { @Test @@ -169,9 +170,8 @@ void typeHintHasAppropriateToString() { void builtWithAppliesMemberCategories() { TypeHint.Builder builder = new TypeHint.Builder(TypeReference.of(String.class)); assertThat(builder.build().getMemberCategories()).isEmpty(); - TypeHint.builtWith(MemberCategory.DECLARED_CLASSES, MemberCategory.DECLARED_FIELDS).accept(builder); - assertThat(builder.build().getMemberCategories()).containsExactlyInAnyOrder(MemberCategory.DECLARED_CLASSES, - MemberCategory.DECLARED_FIELDS); + TypeHint.builtWith(MemberCategory.DECLARED_FIELDS).accept(builder); + assertThat(builder.build().getMemberCategories()).containsExactly(MemberCategory.DECLARED_FIELDS); } } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java index 5bc8f852fe5c..9ea7122ed022 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/ReflectiveRuntimeHintsRegistrarTests.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. @@ -152,7 +152,7 @@ void shouldProcessDifferentAnnotationsOnTypeAndField() { void shouldInvokeCustomProcessor() { process(SampleCustomProcessor.class); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(SampleCustomProcessor.class, "managed")).accepts(this.runtimeHints); + .onMethodInvocation(SampleCustomProcessor.class, "managed")).accepts(this.runtimeHints); assertThat(RuntimeHintsPredicates.reflection().onType(String.class) .withMemberCategory(MemberCategory.INVOKE_DECLARED_METHODS)).accepts(this.runtimeHints); diff --git a/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java b/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java index f19fdb5227d7..1d60f83f54ac 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/annotation/RegisterReflectionForBindingProcessorTests.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 @@ void registerReflectionForBindingOnClass() { processor.registerReflectionHints(hints.reflection(), ClassLevelAnnotatedBean.class); assertThat(RuntimeHintsPredicates.reflection().onType(SampleClassWithGetter.class)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(String.class)).accepts(hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleClassWithGetter.class, "getName")).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleClassWithGetter.class, "getName")).accepts(hints); } @Test @@ -49,7 +49,7 @@ void registerReflectionForBindingOnMethod() throws NoSuchMethodException { processor.registerReflectionHints(hints.reflection(), MethodLevelAnnotatedBean.class.getMethod("method")); assertThat(RuntimeHintsPredicates.reflection().onType(SampleClassWithGetter.class)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(String.class)).accepts(hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleClassWithGetter.class, "getName")).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleClassWithGetter.class, "getName")).accepts(hints); } @Test @@ -57,7 +57,7 @@ void registerReflectionForBindingOnClassItself() { processor.registerReflectionHints(hints.reflection(), SampleClassWithoutAnnotationAttribute.class); assertThat(RuntimeHintsPredicates.reflection().onType(SampleClassWithoutAnnotationAttribute.class)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(String.class)).accepts(hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleClassWithoutAnnotationAttribute.class, "getName")).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleClassWithoutAnnotationAttribute.class, "getName")).accepts(hints); } @Test diff --git a/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java b/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java index 896da82a60dd..d1d577a1b25e 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/predicate/ReflectionHintsPredicatesTests.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,6 +40,7 @@ * * @author Brian Clozel */ +@SuppressWarnings("removal") class ReflectionHintsPredicatesTests { private static Constructor privateConstructor; @@ -155,147 +156,80 @@ void typeWithAnyMemberCategoryDoesNotMatchOtherCategory() { @Nested class ReflectionOnConstructor { - @Test - void constructorIntrospectionDoesNotMatchMissingHint() { - assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).introspect()); - } - - @Test - void constructorIntrospectionMatchesConstructorHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> - typeHint.withConstructor(Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); - } - - @Test - void constructorIntrospectionMatchesIntrospectPublicConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); - } - - @Test - void constructorIntrospectionMatchesInvokePublicConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); - } - - @Test - void constructorIntrospectionMatchesIntrospectDeclaredConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); - } - - @Test - void constructorIntrospectionMatchesInvokeDeclaredConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(publicConstructor).introspect()); - } - @Test void constructorInvocationDoesNotMatchConstructorHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint. withConstructor(Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(publicConstructor)); } @Test void constructorInvocationMatchesConstructorInvocationHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint. withConstructor(Collections.emptyList(), ExecutableMode.INVOKE)); - assertPredicateMatches(reflection.onConstructor(publicConstructor).invoke()); + assertPredicateMatches(reflection.onConstructorInvocation(publicConstructor)); } @Test void constructorInvocationDoesNotMatchIntrospectPublicConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(publicConstructor)); } @Test void constructorInvocationMatchesInvokePublicConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(publicConstructor).invoke()); + assertPredicateMatches(reflection.onConstructorInvocation(publicConstructor)); } @Test void constructorInvocationDoesNotMatchIntrospectDeclaredConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(publicConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(publicConstructor)); } @Test void constructorInvocationMatchesInvokeDeclaredConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(publicConstructor).invoke()); - } - - @Test - void privateConstructorIntrospectionMatchesConstructorHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> - typeHint.withConstructor(TypeReference.listOf(String.class), ExecutableMode.INTROSPECT)); - assertPredicateMatches(reflection.onConstructor(privateConstructor).introspect()); - } - - @Test - void privateConstructorIntrospectionDoesNotMatchIntrospectPublicConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).introspect()); - } - - @Test - void privateConstructorIntrospectionDoesNotMatchInvokePublicConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).introspect()); - } - - @Test - void privateConstructorIntrospectionMatchesIntrospectDeclaredConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(privateConstructor).introspect()); - } - - @Test - void privateConstructorIntrospectionMatchesInvokeDeclaredConstructors() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(privateConstructor).introspect()); + assertPredicateMatches(reflection.onConstructorInvocation(publicConstructor)); } @Test void privateConstructorInvocationDoesNotMatchConstructorHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(TypeReference.listOf(String.class), ExecutableMode.INTROSPECT)); - assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(privateConstructor)); } @Test void privateConstructorInvocationMatchesConstructorInvocationHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withConstructor(TypeReference.listOf(String.class), ExecutableMode.INVOKE)); - assertPredicateMatches(reflection.onConstructor(privateConstructor).invoke()); + assertPredicateMatches(reflection.onConstructorInvocation(privateConstructor)); } @Test void privateConstructorInvocationDoesNotMatchIntrospectPublicConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(privateConstructor)); } @Test void privateConstructorInvocationDoesNotMatchInvokePublicConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(privateConstructor)); } @Test void privateConstructorInvocationDoesNotMatchIntrospectDeclaredConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS); - assertPredicateDoesNotMatch(reflection.onConstructor(privateConstructor).invoke()); + assertPredicateDoesNotMatch(reflection.onConstructorInvocation(privateConstructor)); } @Test void privateConstructorInvocationMatchesInvokeDeclaredConstructors() { runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); - assertPredicateMatches(reflection.onConstructor(privateConstructor).invoke()); + assertPredicateMatches(reflection.onConstructorInvocation(privateConstructor)); } } @@ -303,6 +237,12 @@ void privateConstructorInvocationMatchesInvokeDeclaredConstructors() { @Nested class ReflectionOnMethod { + @Test + void methodIntrospectionMatchesTypeHint() { + runtimeHints.reflection().registerType(SampleClass.class); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); + } + @Test void methodIntrospectionMatchesMethodHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> @@ -328,18 +268,6 @@ void methodIntrospectionMatchesInvokePublicMethods() { assertPredicateMatches(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); } - @Test - void methodIntrospectionDoesNotMatchIntrospectDeclaredMethods() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_DECLARED_METHODS); - assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); - } - - @Test - void methodIntrospectionDoesNotMatchInvokeDeclaredMethods() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_DECLARED_METHODS); - assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "publicMethod").introspect()); - } - @Test void methodInvocationDoesNotMatchMethodHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> @@ -379,22 +307,16 @@ void methodInvocationDoesNotMatchInvokeDeclaredMethods() { } @Test - void privateMethodIntrospectionMatchesMethodHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> - typeHint.withMethod("privateMethod", Collections.emptyList(), ExecutableMode.INTROSPECT)); + void privateMethodIntrospectionMatchesTypeHint() { + runtimeHints.reflection().registerType(SampleClass.class); assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); } @Test - void privateMethodIntrospectionDoesNotMatchIntrospectPublicMethods() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INTROSPECT_PUBLIC_METHODS); - assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); - } - - @Test - void privateMethodIntrospectionDoesNotMatchInvokePublicMethods() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.INVOKE_PUBLIC_METHODS); - assertPredicateDoesNotMatch(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); + void privateMethodIntrospectionMatchesMethodHint() { + runtimeHints.reflection().registerType(SampleClass.class, typeHint -> + typeHint.withMethod("privateMethod", Collections.emptyList(), ExecutableMode.INTROSPECT)); + assertPredicateMatches(reflection.onMethod(SampleClass.class, "privateMethod").introspect()); } @Test @@ -459,49 +381,55 @@ void shouldFailForMissingField() { @Test void shouldFailForUnknownClass() { - assertThatThrownBy(() -> reflection.onField("com.example.DoesNotExist", "missingField")) + assertThatThrownBy(() -> reflection.onFieldAccess("com.example.DoesNotExist", "missingField")) .isInstanceOf(ClassNotFoundException.class); } @Test - void fieldReflectionMatchesFieldHint() { + void publicFieldAccessMatchesFieldHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField")); assertPredicateMatches(reflection.onField(SampleClass.class, "publicField")); } @Test - void fieldReflectionDoesNotMatchNonRegisteredFielddHint() { - runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("publicField")); - assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "privateField")); + void publicFieldAccessMatchesPublicFieldsHint() { + runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.PUBLIC_FIELDS); + assertPredicateMatches(reflection.onField(SampleClass.class, "publicField")); } @Test - void fieldReflectionMatchesPublicFieldsHint() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.PUBLIC_FIELDS); + void publicFieldAccessMatchesAccessPublicFieldsHint() { + runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.ACCESS_PUBLIC_FIELDS); assertPredicateMatches(reflection.onField(SampleClass.class, "publicField")); } @Test - void fieldReflectionDoesNotMatchDeclaredFieldsHint() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.DECLARED_FIELDS); + void fieldAccessDoesNotMatchTypeHint() { + runtimeHints.reflection().registerType(SampleClass.class); assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "publicField")); } @Test - void privateFieldReflectionMatchesFieldHint() { + void privateFieldAccessDoesNotMatchTypeHint() { + runtimeHints.reflection().registerType(SampleClass.class); + assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "privateField")); + } + + @Test + void privateFieldAccessMatchesFieldHint() { runtimeHints.reflection().registerType(SampleClass.class, typeHint -> typeHint.withField("privateField")); assertPredicateMatches(reflection.onField(SampleClass.class, "privateField")); } @Test - void privateFieldReflectionDoesNotMatchPublicFieldsHint() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.PUBLIC_FIELDS); - assertPredicateDoesNotMatch(reflection.onField(SampleClass.class, "privateField")); + void privateFieldAccessMatchesDeclaredFieldsHint() { + runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.DECLARED_FIELDS); + assertPredicateMatches(reflection.onField(SampleClass.class, "privateField")); } @Test - void privateFieldReflectionMatchesDeclaredFieldsHint() { - runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.DECLARED_FIELDS); + void privateFieldAccessMatchesAccessDeclaredFieldsHint() { + runtimeHints.reflection().registerType(SampleClass.class, MemberCategory.ACCESS_DECLARED_FIELDS); assertPredicateMatches(reflection.onField(SampleClass.class, "privateField")); } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java index c34af3867126..a781a6cb873b 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/FilePatternResourceHintsRegistrarTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -78,7 +78,7 @@ void registerWithMultipleFilePrefixes() { @Test void registerWithMultipleClasspathLocations() { - FilePatternResourceHintsRegistrar.forClassPathLocations("").withClasspathLocations("META-INF") + FilePatternResourceHintsRegistrar.forClassPathLocations("").withClassPathLocations("META-INF") .withFilePrefixes("test").withFileExtensions(".txt") .registerHints(this.hints, null); assertThat(this.hints.resourcePatternHints()).singleElement() @@ -133,18 +133,15 @@ void registerWithClasspathLocationUsingResourceClasspathPrefixAndTrailingSlash() @Test void registerWithNonExistingLocationDoesNotRegisterHint() { FilePatternResourceHintsRegistrar.forClassPathLocations("does-not-exist/") - .withClasspathLocations("another-does-not-exist/") + .withClassPathLocations("another-does-not-exist/") .withFilePrefixes("test").withFileExtensions(".txt") .registerHints(this.hints, null); assertThat(this.hints.resourcePatternHints()).isEmpty(); } private Consumer includes(String... patterns) { - return hint -> { - assertThat(hint.getIncludes().stream().map(ResourcePatternHint::getPattern)) - .containsExactlyInAnyOrder(patterns); - assertThat(hint.getExcludes()).isEmpty(); - }; + return hint -> assertThat(hint.getIncludes().stream().map(ResourcePatternHint::getPattern)) + .containsExactlyInAnyOrder(patterns); } } diff --git a/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java b/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java index af23f4802e3e..e01467e60225 100644 --- a/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.java +++ b/spring-core/src/test/java/org/springframework/aot/hint/support/ObjectToObjectConverterRuntimeHintsTests.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. @@ -49,13 +49,13 @@ void setup() { @Test void javaSqlDateHasHints() throws NoSuchMethodException { - assertThat(RuntimeHintsPredicates.reflection().onMethod(java.sql.Date.class, "toLocalDate")).accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(java.sql.Date.class, "toLocalDate")).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(java.sql.Date.class.getMethod("valueOf", LocalDate.class))).accepts(this.hints); } @Test void uriHasHints() throws NoSuchMethodException { - assertThat(RuntimeHintsPredicates.reflection().onConstructor(URI.class.getConstructor(String.class))).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection().onType(URI.class)).accepts(this.hints); } } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java index db6d82a421af..22d0d01ddaff 100644 --- a/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java +++ b/spring-core/src/test/java/org/springframework/aot/nativex/FileNativeConfigurationWriterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,6 @@ import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Path; -import java.util.Collections; -import java.util.List; import java.util.function.Consumer; import java.util.function.Function; @@ -40,7 +38,6 @@ import org.springframework.aot.hint.SerializationHints; import org.springframework.aot.hint.TypeReference; import org.springframework.core.codec.StringDecoder; -import org.springframework.util.MimeType; import static org.assertj.core.api.Assertions.assertThat; @@ -74,10 +71,13 @@ void serializationConfig() throws IOException, JSONException { serializationHints.registerType(Long.class); generator.write(hints); assertEquals(""" - [ - { "name": "java.lang.Integer" }, - { "name": "java.lang.Long" } - ]""", "serialization-config.json"); + { + "serialization": [ + { "type": "java.lang.Integer" }, + { "type": "java.lang.Long" } + ] + } + """); } @Test @@ -89,10 +89,13 @@ void proxyConfig() throws IOException, JSONException { proxyHints.registerJdkProxy(Function.class, Consumer.class); generator.write(hints); assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] }, - { "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] } - ]""", "proxy-config.json"); + { + "reflection": [ + { type: {"proxy": [ "java.util.function.Function" ] } }, + { type: {"proxy": [ "java.util.function.Function", "java.util.function.Consumer" ] } } + ] + } + """); } @Test @@ -102,48 +105,36 @@ void reflectionConfig() throws IOException, JSONException { ReflectionHints reflectionHints = hints.reflection(); reflectionHints.registerType(StringDecoder.class, builder -> builder .onReachableType(String.class) - .withMembers(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS, - MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, + .withMembers(MemberCategory.ACCESS_PUBLIC_FIELDS, MemberCategory.ACCESS_DECLARED_FIELDS, MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES) + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS) .withField("DEFAULT_CHARSET") .withField("defaultCharset") - .withConstructor(TypeReference.listOf(List.class, boolean.class, MimeType.class), ExecutableMode.INTROSPECT) - .withMethod("setDefaultCharset", TypeReference.listOf(Charset.class), ExecutableMode.INVOKE) - .withMethod("getDefaultCharset", Collections.emptyList(), ExecutableMode.INTROSPECT)); + .withMethod("setDefaultCharset", TypeReference.listOf(Charset.class), ExecutableMode.INVOKE)); generator.write(hints); assertEquals(""" - [ - { - "name": "org.springframework.core.codec.StringDecoder", - "condition": { "typeReachable": "java.lang.String" }, - "allPublicFields": true, - "allDeclaredFields": true, - "queryAllPublicConstructors": true, - "queryAllDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredConstructors": true, - "queryAllPublicMethods": true, - "queryAllDeclaredMethods": true, - "allPublicMethods": true, - "allDeclaredMethods": true, - "allPublicClasses": true, - "allDeclaredClasses": true, - "fields": [ - { "name": "DEFAULT_CHARSET" }, - { "name": "defaultCharset" } - ], - "methods": [ - { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } - ], - "queriedMethods": [ - { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name": "getDefaultCharset", "parameterTypes": [ ] } - ] - } - ]""", "reflect-config.json"); + { + "reflection": [ + { + "type": "org.springframework.core.codec.StringDecoder", + "condition": { "typeReached": "java.lang.String" }, + "allPublicFields": true, + "allDeclaredFields": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "fields": [ + { "name": "DEFAULT_CHARSET" }, + { "name": "defaultCharset" } + ], + "methods": [ + { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } + ] + } + ] + } + """); } @Test @@ -155,12 +146,14 @@ void jniConfig() throws IOException, JSONException { jniHints.registerType(StringDecoder.class, builder -> builder.onReachableType(String.class)); generator.write(hints); assertEquals(""" - [ - { - "name": "org.springframework.core.codec.StringDecoder", - "condition": { "typeReachable": "java.lang.String" } - } - ]""", "jni-config.json"); + { + "jni": [ + { + "type": "org.springframework.core.codec.StringDecoder", + "condition": { "typeReached": "java.lang.String" } + } + ] + }"""); } @Test @@ -173,23 +166,21 @@ void resourceConfig() throws IOException, JSONException { generator.write(hints); assertEquals(""" { - "resources": { - "includes": [ - {"pattern": "\\\\Qcom/example/test.properties\\\\E"}, - {"pattern": "\\\\Q/\\\\E"}, - {"pattern": "\\\\Qcom\\\\E"}, - {"pattern": "\\\\Qcom/example\\\\E"}, - {"pattern": "\\\\Qcom/example/another.properties\\\\E"} - ] - } - }""", "resource-config.json"); + "resources": [ + {"glob": "com/example/test.properties"}, + {"glob": "/"}, + {"glob": "com"}, + {"glob": "com/example"}, + {"glob": "com/example/another.properties"} + ] + }"""); } @Test void namespace() { String groupId = "foo.bar"; String artifactId = "baz"; - String filename = "resource-config.json"; + String filename = "reachability-metadata.json"; FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir, groupId, artifactId); RuntimeHints hints = new RuntimeHints(); ResourceHints resourceHints = hints.resources(); @@ -199,8 +190,8 @@ void namespace() { assertThat(jsonFile.toFile()).exists(); } - private void assertEquals(String expectedString, String filename) throws IOException, JSONException { - Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve(filename); + private void assertEquals(String expectedString) throws IOException, JSONException { + Path jsonFile = tempDir.resolve("META-INF").resolve("native-image").resolve("reachability-metadata.json"); String content = Files.readString(jsonFile); JSONAssert.assertEquals(expectedString, content, JSONCompareMode.NON_EXTENSIBLE); } diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java deleted file mode 100644 index 6a65db7e9d10..000000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.aot.nativex; - -import java.io.StringWriter; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.ProxyHints; -import org.springframework.aot.hint.TypeReference; - -/** - * Tests for {@link ProxyHintsWriter}. - * - * @author Sebastien Deleuze - * @author Stephane Nicoll - */ -class ProxyHintsWriterTests { - - @Test - void empty() throws JSONException { - ProxyHints hints = new ProxyHints(); - assertEquals("[]", hints); - } - - @Test - void shouldWriteOneEntry() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Function.class); - assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] } - ]""", hints); - } - - @Test - void shouldWriteMultipleEntries() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Function.class); - hints.registerJdkProxy(Function.class, Consumer.class); - assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] }, - { "interfaces": [ "java.util.function.Function", "java.util.function.Consumer" ] } - ]""", hints); - } - - @Test - void shouldWriteEntriesInNaturalOrder() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Supplier.class); - hints.registerJdkProxy(Function.class); - assertEquals(""" - [ - { "interfaces": [ "java.util.function.Function" ] }, - { "interfaces": [ "java.util.function.Supplier" ] } - ]""", hints); - } - - @Test - void shouldWriteInnerClass() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(Inner.class); - assertEquals(""" - [ - { "interfaces": [ "org.springframework.aot.nativex.ProxyHintsWriterTests$Inner" ] } - ]""", hints); - } - - @Test - void shouldWriteCondition() throws JSONException { - ProxyHints hints = new ProxyHints(); - hints.registerJdkProxy(builder -> builder.proxiedInterfaces(Function.class) - .onReachableType(TypeReference.of("org.example.Test"))); - assertEquals(""" - [ - { "condition": { "typeReachable": "org.example.Test"}, "interfaces": [ "java.util.function.Function" ] } - ]""", hints); - } - - private void assertEquals(String expectedString, ProxyHints hints) throws JSONException { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - ProxyHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); - } - - interface Inner { - - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java deleted file mode 100644 index 0e797e390915..000000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ReflectionHintsWriterTests.java +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.aot.nativex; - -import java.io.StringWriter; -import java.nio.charset.Charset; -import java.util.Collections; -import java.util.List; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.ExecutableMode; -import org.springframework.aot.hint.MemberCategory; -import org.springframework.aot.hint.ReflectionHints; -import org.springframework.aot.hint.TypeReference; -import org.springframework.core.codec.StringDecoder; -import org.springframework.util.MimeType; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ReflectionHintsWriter}. - * - * @author Sebastien Deleuze - * @author Stephane Nicoll - */ -class ReflectionHintsWriterTests { - - @Test - void empty() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - assertEquals("[]", hints); - } - - @Test - void one() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(StringDecoder.class, builder -> builder - .onReachableType(String.class) - .withMembers(MemberCategory.PUBLIC_FIELDS, MemberCategory.DECLARED_FIELDS, - MemberCategory.INTROSPECT_PUBLIC_CONSTRUCTORS, MemberCategory.INTROSPECT_DECLARED_CONSTRUCTORS, - MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.INTROSPECT_PUBLIC_METHODS, MemberCategory.INTROSPECT_DECLARED_METHODS, - MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, - MemberCategory.PUBLIC_CLASSES, MemberCategory.DECLARED_CLASSES, MemberCategory.UNSAFE_ALLOCATED) - .withField("DEFAULT_CHARSET") - .withField("defaultCharset") - .withField("aScore") - .withConstructor(TypeReference.listOf(List.class, boolean.class, MimeType.class), ExecutableMode.INTROSPECT) - .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE) - .withMethod("getDefaultCharset", Collections.emptyList(), ExecutableMode.INTROSPECT)); - assertEquals(""" - [ - { - "name": "org.springframework.core.codec.StringDecoder", - "condition": { "typeReachable": "java.lang.String" }, - "allPublicFields": true, - "allDeclaredFields": true, - "queryAllPublicConstructors": true, - "queryAllDeclaredConstructors": true, - "allPublicConstructors": true, - "allDeclaredConstructors": true, - "queryAllPublicMethods": true, - "queryAllDeclaredMethods": true, - "allPublicMethods": true, - "allDeclaredMethods": true, - "allPublicClasses": true, - "allDeclaredClasses": true, - "unsafeAllocated": true, - "fields": [ - { "name": "aScore" }, - { "name": "DEFAULT_CHARSET" }, - { "name": "defaultCharset" } - ], - "methods": [ - { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } - ], - "queriedMethods": [ - { "name": "", "parameterTypes": [ "java.util.List", "boolean", "org.springframework.util.MimeType" ] }, - { "name": "getDefaultCharset", "parameterTypes": [ ] } - ] - } - ]""", hints); - } - - @Test - void two() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - }); - hints.registerType(Long.class, builder -> { - }); - - assertEquals(""" - [ - { "name": "java.lang.Integer" }, - { "name": "java.lang.Long" } - ]""", hints); - } - - @Test - void queriedMethods() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), ExecutableMode.INTROSPECT)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "queriedMethods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String"] - } - ] - } - ] - """, hints); - } - - @Test - void methods() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), ExecutableMode.INVOKE)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "methods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String"] - } - ] - } - ] - """, hints); - } - - @Test - void methodWithInnerClassParameter() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("test", - TypeReference.listOf(Inner.class), ExecutableMode.INVOKE)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "methods": [ - { - "name": "test", - "parameterTypes": ["org.springframework.aot.nativex.ReflectionHintsWriterTests$Inner"] - } - ] - } - ] - """, hints); - } - - @Test - void methodAndQueriedMethods() throws JSONException { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class), ExecutableMode.INVOKE)); - hints.registerType(Integer.class, builder -> builder.withMethod("parseInt", - TypeReference.listOf(String.class, int.class), ExecutableMode.INTROSPECT)); - - assertEquals(""" - [ - { - "name": "java.lang.Integer", - "queriedMethods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String", "int"] - } - ], - "methods": [ - { - "name": "parseInt", - "parameterTypes": ["java.lang.String"] - } - ] - } - ] - """, hints); - } - - @Test - void ignoreLambda() throws JSONException { - Runnable anonymousRunnable = () -> {}; - ReflectionHints hints = new ReflectionHints(); - hints.registerType(anonymousRunnable.getClass()); - assertEquals("[]", hints); - } - - @Test - void sortTypeHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> {}); - hints.registerType(Long.class, builder -> {}); - - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Long.class, builder -> {}); - hints2.registerType(Integer.class, builder -> {}); - - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - @Test - void sortFieldHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - builder.withField("first"); - builder.withField("second"); - }); - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Integer.class, builder -> { - builder.withField("second"); - builder.withField("first"); - }); - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - @Test - void sortConstructorHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); - builder.withConstructor(List.of(TypeReference.of(String.class), - TypeReference.of(Integer.class)), ExecutableMode.INVOKE); - }); - - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Integer.class, builder -> { - builder.withConstructor(List.of(TypeReference.of(String.class), - TypeReference.of(Integer.class)), ExecutableMode.INVOKE); - builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); - }); - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - @Test - void sortMethodHints() { - ReflectionHints hints = new ReflectionHints(); - hints.registerType(Integer.class, builder -> { - builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); - builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); - }); - - ReflectionHints hints2 = new ReflectionHints(); - hints2.registerType(Integer.class, builder -> { - builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); - builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); - }); - assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); - } - - private void assertEquals(String expectedString, ReflectionHints hints) throws JSONException { - JSONAssert.assertEquals(expectedString, writeJson(hints), JSONCompareMode.STRICT); - } - - private String writeJson(ReflectionHints hints) { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - ReflectionHintsWriter.INSTANCE.write(writer, hints); - return out.toString(); - } - - - static class Inner { - - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java deleted file mode 100644 index b3fef587efa1..000000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/ResourceHintsWriterTests.java +++ /dev/null @@ -1,190 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.aot.nativex; - -import java.io.StringWriter; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.ResourceHints; -import org.springframework.aot.hint.TypeReference; - -/** - * Tests for {@link ResourceHintsWriter}. - * - * @author Sebastien Deleuze - * @author Brian Clozel - */ -class ResourceHintsWriterTests { - - @Test - void empty() throws JSONException { - ResourceHints hints = new ResourceHints(); - assertEquals("{}", hints); - } - - @Test - void registerExactMatch() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("com/example/test.properties"); - hints.registerPattern("com/example/another.properties"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/another.properties\\\\E"}, - { "pattern": "\\\\Qcom/example/test.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWildcardAtTheBeginningPattern() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("*.properties"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": ".*\\\\Q.properties\\\\E"}, - { "pattern": "\\\\Q\\/\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWildcardInTheMiddlePattern() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("com/example/*.properties"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWildcardAtTheEndPattern() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern("static/*"); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qstatic\\\\E"}, - { "pattern": "\\\\Qstatic/\\\\E.*"} - ] - } - }""", hints); - } - - @Test - void registerPatternWithIncludesAndExcludes() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern(hint -> hint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties")); - hints.registerPattern(hint -> hint.includes("org/other/*.properties").excludes("org/other/to-ignore.properties")); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E"}, - { "pattern": "\\\\Qcom\\\\E"}, - { "pattern": "\\\\Qcom/example\\\\E"}, - { "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"}, - { "pattern": "\\\\Qorg\\\\E"}, - { "pattern": "\\\\Qorg/other\\\\E"}, - { "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"} - ], - "excludes": [ - { "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"}, - { "pattern": "\\\\Qorg/other/to-ignore.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerWithReachableTypeCondition() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerPattern(builder -> builder.includes(TypeReference.of("com.example.Test"), "com/example/test.properties")); - assertEquals(""" - { - "resources": { - "includes": [ - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Q/\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example\\\\E"}, - { "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"} - ] - } - }""", hints); - } - - @Test - void registerType() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerType(String.class); - assertEquals(""" - { - "resources": { - "includes": [ - { "pattern": "\\\\Q/\\\\E" }, - { "pattern": "\\\\Qjava\\\\E" }, - { "pattern": "\\\\Qjava/lang\\\\E" }, - { "pattern": "\\\\Qjava/lang/String.class\\\\E" } - ] - } - }""", hints); - } - - @Test - void registerResourceBundle() throws JSONException { - ResourceHints hints = new ResourceHints(); - hints.registerResourceBundle("com.example.message2"); - hints.registerResourceBundle("com.example.message"); - assertEquals(""" - { - "bundles": [ - { "name": "com.example.message"}, - { "name": "com.example.message2"} - ] - }""", hints); - } - - private void assertEquals(String expectedString, ResourceHints hints) throws JSONException { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - ResourceHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); - } - -} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java new file mode 100644 index 000000000000..89eab1cd0670 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/aot/nativex/RuntimeHintsWriterTests.java @@ -0,0 +1,596 @@ +/* + * 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.aot.nativex; + +import java.io.StringWriter; +import java.nio.charset.Charset; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.networknt.schema.InputFormat; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SchemaLocation; +import com.networknt.schema.SchemaValidatorsConfig; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import org.json.JSONException; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.skyscreamer.jsonassert.JSONAssert; +import org.skyscreamer.jsonassert.JSONCompareMode; + +import org.springframework.aot.hint.ExecutableMode; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.core.codec.StringDecoder; +import org.springframework.core.env.Environment; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link RuntimeHintsWriter}. + * + * @author Brian Clozel + * @author Sebastien Deleuze + * @author Stephane Nicoll + */ +class RuntimeHintsWriterTests { + + private static JsonSchema JSON_SCHEMA; + + @BeforeAll + static void setupSchemaValidator() { + JsonSchemaFactory jsonSchemaFactory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V201909, builder -> + builder.schemaMappers(schemaMappers -> schemaMappers.mapPrefix("https://www.graalvm.org/", "classpath:org/springframework/aot/nativex/")) + ); + SchemaValidatorsConfig config = SchemaValidatorsConfig.builder().build(); + JSON_SCHEMA = jsonSchemaFactory.getSchema(SchemaLocation.of("https://www.graalvm.org/reachability-metadata-schema-v1.0.0.json"), config); + } + + @Nested + class ReflectionHintsTests { + + @Test + void empty() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void one() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(StringDecoder.class, builder -> builder + .onReachableType(String.class) + .withMembers(MemberCategory.ACCESS_PUBLIC_FIELDS, MemberCategory.ACCESS_DECLARED_FIELDS, + MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_PUBLIC_METHODS, MemberCategory.INVOKE_DECLARED_METHODS, + MemberCategory.UNSAFE_ALLOCATED) + .withField("DEFAULT_CHARSET") + .withField("defaultCharset") + .withField("aScore") + .withMethod("setDefaultCharset", List.of(TypeReference.of(Charset.class)), ExecutableMode.INVOKE)); + assertEquals(""" + { + "reflection": [ + { + "type": "org.springframework.core.codec.StringDecoder", + "condition": { "typeReached": "java.lang.String" }, + "allPublicFields": true, + "allDeclaredFields": true, + "allPublicConstructors": true, + "allDeclaredConstructors": true, + "allPublicMethods": true, + "allDeclaredMethods": true, + "unsafeAllocated": true, + "fields": [ + { "name": "aScore" }, + { "name": "DEFAULT_CHARSET" }, + { "name": "defaultCharset" } + ], + "methods": [ + { "name": "setDefaultCharset", "parameterTypes": [ "java.nio.charset.Charset" ] } + ] + } + ] + } + """, hints); + } + + @Test + void two() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + }); + hints.reflection().registerType(Long.class, builder -> { + }); + + assertEquals(""" + { + "reflection": [ + { "type": "java.lang.Integer" }, + { "type": "java.lang.Long" } + ] + } + """, hints); + } + + @Test + void methods() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> builder.withMethod("parseInt", + TypeReference.listOf(String.class), ExecutableMode.INVOKE)); + + assertEquals(""" + { + "reflection": [ + { + "type": "java.lang.Integer", + "methods": [ + { + "name": "parseInt", + "parameterTypes": ["java.lang.String"] + } + ] + } + ] + } + """, hints); + } + + @Test + void methodWithInnerClassParameter() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> builder.withMethod("test", + TypeReference.listOf(InnerClass.class), ExecutableMode.INVOKE)); + + assertEquals(""" + { + "reflection": [ + { + "type": "java.lang.Integer", + "methods": [ + { + "name": "test", + "parameterTypes": ["org.springframework.aot.nativex.RuntimeHintsWriterTests$InnerClass"] + } + ] + } + ] + } + """, hints); + } + + @Test + void methodAndQueriedMethods() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> builder.withMethod("parseInt", + TypeReference.listOf(String.class), ExecutableMode.INVOKE)); + + assertEquals(""" + { + "reflection": [ + { + "type": "java.lang.Integer", + "methods": [ + { + "name": "parseInt", + "parameterTypes": ["java.lang.String"] + } + ] + } + ] + } + """, hints); + } + + @Test + void ignoreLambda() throws JSONException { + Runnable anonymousRunnable = () -> {}; + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(anonymousRunnable.getClass()); + assertEquals("{}", hints); + } + + @Test + void sortTypeHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> {}); + hints.reflection().registerType(Long.class, builder -> {}); + + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Long.class, builder -> {}); + hints2.reflection().registerType(Integer.class, builder -> {}); + + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortFieldHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + builder.withField("first"); + builder.withField("second"); + }); + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Integer.class, builder -> { + builder.withField("second"); + builder.withField("first"); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortConstructorHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); + builder.withConstructor(List.of(TypeReference.of(String.class), + TypeReference.of(Integer.class)), ExecutableMode.INVOKE); + }); + + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Integer.class, builder -> { + builder.withConstructor(List.of(TypeReference.of(String.class), + TypeReference.of(Integer.class)), ExecutableMode.INVOKE); + builder.withConstructor(List.of(TypeReference.of(String.class)), ExecutableMode.INVOKE); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + @Test + void sortMethodHints() { + RuntimeHints hints = new RuntimeHints(); + hints.reflection().registerType(Integer.class, builder -> { + builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); + builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); + }); + + RuntimeHints hints2 = new RuntimeHints(); + hints2.reflection().registerType(Integer.class, builder -> { + builder.withMethod("another", Collections.emptyList(), ExecutableMode.INVOKE); + builder.withMethod("test", Collections.emptyList(), ExecutableMode.INVOKE); + }); + assertThat(writeJson(hints)).isEqualTo(writeJson(hints2)); + } + + } + + + @Nested + class JniHints { + + // TODO + + } + + + @Nested + class ResourceHintsTests { + + @Test + void empty() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void registerExactMatch() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("com/example/test.properties"); + hints.resources().registerPattern("com/example/another.properties"); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "com"}, + { "glob": "com/example"}, + { "glob": "com/example/another.properties"}, + { "glob": "com/example/test.properties"} + ] + }""", hints); + } + + @Test + void registerWildcardAtTheBeginningPattern() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("*.properties"); + assertEquals(""" + { + "resources": [ + { "glob": "*.properties"}, + { "glob": "/"} + ] + }""", hints); + } + + @Test + void registerWildcardInTheMiddlePattern() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("com/example/*.properties"); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "com"}, + { "glob": "com/example"}, + { "glob": "com/example/*.properties"} + ] + }""", hints); + } + + @Test + void registerWildcardAtTheEndPattern() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern("static/*"); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "static"}, + { "glob": "static/*"} + ] + }""", hints); + } + + @Test + void registerPatternWithIncludesAndExcludes() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern(hint -> hint.includes("com/example/*.properties")); + hints.resources().registerPattern(hint -> hint.includes("org/other/*.properties")); + assertEquals(""" + { + "resources": [ + { "glob": "/"}, + { "glob": "com"}, + { "glob": "com/example"}, + { "glob": "com/example/*.properties"}, + { "glob": "org"}, + { "glob": "org/other"}, + { "glob": "org/other/*.properties"} + ] + }""", hints); + } + + @Test + void registerWithReachableTypeCondition() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerPattern(builder -> builder.includes(TypeReference.of("com.example.Test"), "com/example/test.properties")); + assertEquals(""" + { + "resources": [ + { "condition": { "typeReached": "com.example.Test"}, "glob": "/"}, + { "condition": { "typeReached": "com.example.Test"}, "glob": "com"}, + { "condition": { "typeReached": "com.example.Test"}, "glob": "com/example"}, + { "condition": { "typeReached": "com.example.Test"}, "glob": "com/example/test.properties"} + ] + }""", hints); + } + + @Test + void registerType() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerType(String.class); + assertEquals(""" + { + "resources": [ + { "glob": "/" }, + { "glob": "java" }, + { "glob": "java/lang" }, + { "glob": "java/lang/String.class" } + ] + }""", hints); + } + + @Test + void registerResourceBundle() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.resources().registerResourceBundle("com.example.message2"); + hints.resources().registerResourceBundle("com.example.message"); + assertEquals(""" + { + "bundles": [ + { "name": "com.example.message"}, + { "name": "com.example.message2"} + ] + }""", hints); + } + } + + @Nested + class SerializationHintsTests { + + @Test + void shouldWriteEmptyHint() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void shouldWriteSingleHint() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.serialization().registerType(TypeReference.of(String.class)); + assertEquals(""" + { + "serialization": [ + { "type": "java.lang.String" } + ] + } + """, hints); + } + + @Test + void shouldWriteMultipleHints() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.serialization() + .registerType(TypeReference.of(Environment.class)) + .registerType(TypeReference.of(String.class)); + assertEquals(""" + { + "serialization": [ + { "type": "java.lang.String" }, + { "type": "org.springframework.core.env.Environment" } + ] + } + """, hints); + } + + @Test + void shouldWriteSingleHintWithCondition() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.serialization().registerType(TypeReference.of(String.class), + builder -> builder.onReachableType(TypeReference.of("org.example.Test"))); + assertEquals(""" + { + "serialization": [ + { "condition": { "typeReached": "org.example.Test" }, "type": "java.lang.String" } + ] + } + """, hints); + } + + } + + @Nested + class ProxyHintsTests { + + @Test + void empty() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + assertEquals("{}", hints); + } + + @Test + void shouldWriteOneEntry() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(Function.class); + assertEquals(""" + { + "reflection": [ + { + "type": { + "proxy": ["java.util.function.Function"] + } + } + ] + } + """, hints); + } + + @Test + void shouldWriteMultipleEntries() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(Function.class) + .registerJdkProxy(Function.class, Consumer.class); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["java.util.function.Function"] } + }, + { + "type": { "proxy": ["java.util.function.Function", "java.util.function.Consumer"] } + } + ] + } + """, hints); + } + + @Test + void shouldWriteEntriesInNaturalOrder() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(Supplier.class) + .registerJdkProxy(Function.class); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["java.util.function.Function"] } + }, + { + "type": { "proxy": ["java.util.function.Supplier"] } + } + ] + } + """, hints); + } + + @Test + void shouldWriteInnerClass() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(InnerInterface.class); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["org.springframework.aot.nativex.RuntimeHintsWriterTests$InnerInterface"] } + } + ] + } + """, hints); + } + + @Test + void shouldWriteCondition() throws JSONException { + RuntimeHints hints = new RuntimeHints(); + hints.proxies().registerJdkProxy(builder -> builder.proxiedInterfaces(Function.class) + .onReachableType(TypeReference.of("org.example.Test"))); + assertEquals(""" + { + "reflection": [ + { + "type": { "proxy": ["java.util.function.Function"] }, + "condition": { "typeReached": "org.example.Test" } + } + ] + } + """, hints); + } + + } + + private void assertEquals(String expectedString, RuntimeHints hints) throws JSONException { + String json = writeJson(hints); + JSONAssert.assertEquals(expectedString, json, JSONCompareMode.LENIENT); + Set validationMessages = JSON_SCHEMA.validate(json, InputFormat.JSON, executionContext -> + executionContext.getExecutionConfig().setFormatAssertionsEnabled(true)); + assertThat(validationMessages).isEmpty(); + } + + private String writeJson(RuntimeHints hints) { + StringWriter out = new StringWriter(); + BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); + new RuntimeHintsWriter().write(writer, hints); + return out.toString(); + } + + + static class InnerClass { + + } + + interface InnerInterface { + + } + +} diff --git a/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java b/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java deleted file mode 100644 index bef492224894..000000000000 --- a/spring-core/src/test/java/org/springframework/aot/nativex/SerializationHintsWriterTests.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.aot.nativex; - -import java.io.StringWriter; - -import org.json.JSONException; -import org.junit.jupiter.api.Test; -import org.skyscreamer.jsonassert.JSONAssert; -import org.skyscreamer.jsonassert.JSONCompareMode; - -import org.springframework.aot.hint.SerializationHints; -import org.springframework.aot.hint.TypeReference; -import org.springframework.core.env.Environment; - -/** - * Tests for {@link SerializationHintsWriter}. - * - * @author Sebastien Deleuze - */ -class SerializationHintsWriterTests { - - @Test - void shouldWriteEmptyHint() throws JSONException { - SerializationHints hints = new SerializationHints(); - assertEquals("[]", hints); - } - - @Test - void shouldWriteSingleHint() throws JSONException { - SerializationHints hints = new SerializationHints().registerType(TypeReference.of(String.class)); - assertEquals(""" - [ - { "name": "java.lang.String" } - ]""", hints); - } - - @Test - void shouldWriteMultipleHints() throws JSONException { - SerializationHints hints = new SerializationHints() - .registerType(TypeReference.of(Environment.class)) - .registerType(TypeReference.of(String.class)); - assertEquals(""" - [ - { "name": "java.lang.String" }, - { "name": "org.springframework.core.env.Environment" } - ]""", hints); - } - - @Test - void shouldWriteSingleHintWithCondition() throws JSONException { - SerializationHints hints = new SerializationHints().registerType(TypeReference.of(String.class), - builder -> builder.onReachableType(TypeReference.of("org.example.Test"))); - assertEquals(""" - [ - { "condition": { "typeReachable": "org.example.Test" }, "name": "java.lang.String" } - ]""", hints); - } - - private void assertEquals(String expectedString, SerializationHints hints) throws JSONException { - StringWriter out = new StringWriter(); - BasicJsonWriter writer = new BasicJsonWriter(out, "\t"); - SerializationHintsWriter.INSTANCE.write(writer, hints); - JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT); - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java b/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java index e01f0275cea4..dbbacdc01e34 100644 --- a/spring-core/src/test/java/org/springframework/core/MethodParameterTests.java +++ b/spring-core/src/test/java/org/springframework/core/MethodParameterTests.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 @@ * @author Juergen Hoeller * @author Sam Brannen * @author Phillip Webb + * @author Sebastien Deleuze */ class MethodParameterTests { @@ -49,6 +50,14 @@ class MethodParameterTests { private MethodParameter intReturnType; + private MethodParameter jspecifyNullableParameter; + + private MethodParameter jspecifyNonNullParameter; + + private MethodParameter springNullableParameter; + + private MethodParameter springNonNullParameter; + @BeforeEach void setup() throws NoSuchMethodException { @@ -56,6 +65,12 @@ void setup() throws NoSuchMethodException { stringParameter = new MethodParameter(method, 0); longParameter = new MethodParameter(method, 1); intReturnType = new MethodParameter(method, -1); + Method jspecifyNullableMethod = getClass().getMethod("jspecifyNullableMethod", String.class, String.class); + jspecifyNullableParameter = new MethodParameter(jspecifyNullableMethod, 0); + jspecifyNonNullParameter = new MethodParameter(jspecifyNullableMethod, 1); + Method springNullableMethod = getClass().getMethod("springNullableMethod", String.class, String.class); + springNullableParameter = new MethodParameter(springNullableMethod, 0); + springNonNullParameter = new MethodParameter(springNullableMethod, 1); } @@ -237,10 +252,40 @@ void nestedWithTypeIndexReturnsNewInstance() throws Exception { assertThat(m3.getTypeIndexForCurrentLevel()).isEqualTo(3); } + @Test + void jspecifyNullableParameter() { + assertThat(jspecifyNullableParameter.isOptional()).isTrue(); + } + + @Test + void jspecifyNonNullParameter() { + assertThat(jspecifyNonNullParameter.isOptional()).isFalse(); + } + + @Test + void springNullableParameter() { + assertThat(springNullableParameter.isOptional()).isTrue(); + } + + @Test + void springNonNullParameter() { + assertThat(springNonNullParameter.isOptional()).isFalse(); + } + public int method(String p1, long p2) { return 42; } + public @org.jspecify.annotations.Nullable String jspecifyNullableMethod(@org.jspecify.annotations.Nullable String nullableParameter, String nonNullParameter) { + return nullableParameter; + } + + @SuppressWarnings("deprecation") + @org.springframework.lang.Nullable + public String springNullableMethod(@org.springframework.lang.Nullable String nullableParameter, String nonNullParameter) { + return nullableParameter; + } + @SuppressWarnings("unused") private static class NestedClass { diff --git a/spring-core/src/test/java/org/springframework/core/NullnessTests.java b/spring-core/src/test/java/org/springframework/core/NullnessTests.java new file mode 100644 index 000000000000..a9fef1a98945 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/NullnessTests.java @@ -0,0 +1,396 @@ +/* + * Copyright 2002-2025 the original author 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; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.core.testfixture.nullness.ClassMarkedJSpecifyProcessor; +import org.springframework.core.testfixture.nullness.CustomNullableProcessor; +import org.springframework.core.testfixture.nullness.JSpecifyProcessor; +import org.springframework.core.testfixture.nullness.NullnessFields; +import org.springframework.core.testfixture.nullness.marked.PackageMarkedJSpecifyProcessor; +import org.springframework.core.testfixture.nullness.marked.unmarked.PackageUnmarkedJSpecifyProcessor; + +/** + * Tests for {@link Nullness}. + * + * @author Sebastien Deleuze + */ +public class NullnessTests { + + // JSpecify without @NullMarked and @NullUnmarked + + @Test + void jspecifyUnspecifiedReturnType() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyNullableReturnType() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("nullableProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyNonNullReturnType() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("nonNullProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyUnspecifiedParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyNullableParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[1]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyNonNullParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[2]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // JSpecify with MethodParameter without @NullMarked and @NullUnmarked + + @Test + void jspecifyUnspecifiedReturnTypeWithMethodParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var methodParameter = MethodParameter.forExecutable(method, -1); + var nullness = Nullness.forMethodParameter(methodParameter); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyNullableReturnTypeWithMethodParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("nullableProcess"); + var methodParameter = MethodParameter.forExecutable(method, -1); + var nullness = Nullness.forMethodParameter(methodParameter); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyNonNullReturnTypeWithMethodParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("nonNullProcess"); + var methodParameter = MethodParameter.forExecutable(method, -1); + var nullness = Nullness.forMethodParameter(methodParameter); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyUnspecifiedParameterWithMethodParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var methodParameter = MethodParameter.forExecutable(method, 0); + var nullness = Nullness.forMethodParameter(methodParameter); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyNullableParameterWithMethodParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var methodParameter = MethodParameter.forExecutable(method, 1); + var nullness = Nullness.forMethodParameter(methodParameter); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyNonNullParameterWithMethodParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var methodParameter = MethodParameter.forExecutable(method, 2); + var nullness = Nullness.forMethodParameter(methodParameter); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // JSpecify with Field without @NullMarked and @NullUnmarked + + @Test + void jspecifyUnspecifiedWithField() throws NoSuchFieldException { + var field = NullnessFields.class.getDeclaredField("unannotatedField"); + var nullness = Nullness.forField(field); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyNullableWithField() throws NoSuchFieldException { + var field = NullnessFields.class.getDeclaredField("jspecifyNullableField"); + var nullness = Nullness.forField(field); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyNonNullWithField() throws NoSuchFieldException { + var field = NullnessFields.class.getDeclaredField("jspecifyNonNullField"); + var nullness = Nullness.forField(field); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // JSpecify with method-level @NullMarked + + @Test + void jspecifyMethodMarkedUnspecifiedReturnType() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("markedProcess", String.class, String.class, String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyMethodMarkedNullableReturnType() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("nullableMarkedProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyMethodMarkedNonNullReturnType() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("nonNullMarkedProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyMethodMarkedUnspecifiedParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("markedProcess", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyMethodMarkedNullableParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("markedProcess", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[1]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyMethodMarkedNonNullParameter() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("markedProcess", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[2]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // JSpecify with class-level @NullMarked + + @Test + void jspecifyClassMarkedUnspecifiedReturnType() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyClassMarkedNullableReturnType() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("nullableProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyClassMarkedNonNullReturnType() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("nonNullProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyClassMarkedUnspecifiedParameter() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyClassMarkedNullableParameter() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[1]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyClassMarkedNonNullParameter() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[2]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyClassMarkedMethodUnmarkedUnspecifiedReturnType() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("unmarkedProcess", String.class, String.class, String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyClassMarkedMethodUnmarkedUnspecifiedParameter() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("unmarkedProcess", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyClassMarkedMethodUnmarkedNullableParameter() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("unmarkedProcess", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[1]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyClassMarkedMethodUnmarkedNonNullParameter() throws NoSuchMethodException { + var method = ClassMarkedJSpecifyProcessor.class.getMethod("unmarkedProcess", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[2]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // JSpecify with package-level @NullMarked + + @Test + void jspecifyPackageMarkedUnspecifiedReturnType() throws NoSuchMethodException { + var method = PackageMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyPackageMarkedNullableReturnType() throws NoSuchMethodException { + var method = PackageMarkedJSpecifyProcessor.class.getMethod("nullableProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyPackageMarkedNonNullReturnType() throws NoSuchMethodException { + var method = PackageMarkedJSpecifyProcessor.class.getMethod("nonNullProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyPackageMarkedUnspecifiedParameter() throws NoSuchMethodException { + var method = PackageMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyPackageMarkedNullableParameter() throws NoSuchMethodException { + var method = PackageMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[1]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyPackageMarkedNonNullParameter() throws NoSuchMethodException { + var method = PackageMarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[2]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // JSpecify with package-level @NullUnmarked + + @Test + void jspecifyPackageUnmarkedUnspecifiedReturnType() throws NoSuchMethodException { + var method = PackageUnmarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyPackageUnmarkedNullableReturnType() throws NoSuchMethodException { + var method = PackageUnmarkedJSpecifyProcessor.class.getMethod("nullableProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyPackageUnmarkedNonNullReturnType() throws NoSuchMethodException { + var method = PackageUnmarkedJSpecifyProcessor.class.getMethod("nonNullProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void jspecifyPackageUnmarkedUnspecifiedParameter() throws NoSuchMethodException { + var method = PackageUnmarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + + @Test + void jspecifyPackageUnmarkedNullableParameter() throws NoSuchMethodException { + var method = PackageUnmarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[1]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void jspecifyPackageUnmarkedNonNullParameter() throws NoSuchMethodException { + var method = PackageUnmarkedJSpecifyProcessor.class.getMethod("process", String.class, String.class, String.class); + var nullness = Nullness.forParameter(method.getParameters()[2]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + // Custom @Nullable + + @Test + void customNullableReturnType() throws NoSuchMethodException { + var method = CustomNullableProcessor.class.getMethod("process", String.class); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void customNullableParameter() throws NoSuchMethodException { + var method = CustomNullableProcessor.class.getMethod("process", String.class); + var nullness = Nullness.forParameter(method.getParameters()[0]); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + @Test + void customNullableField() throws NoSuchFieldException { + var field = NullnessFields.class.getDeclaredField("customNullableField"); + var nullness = Nullness.forField(field); + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE); + } + + // Primitive types + + @Test + void primitiveField() throws NoSuchFieldException { + var field = NullnessFields.class.getDeclaredField("primitiveField"); + var nullness = Nullness.forField(field); + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL); + } + + @Test + void voidMethod() throws NoSuchMethodException { + var method = JSpecifyProcessor.class.getMethod("voidProcess"); + var nullness = Nullness.forMethodReturnType(method); + Assertions.assertThat(nullness).isEqualTo(Nullness.UNSPECIFIED); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java index bf6a82bb5751..598ea312995a 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotatedElementUtilsTests.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 javax.annotation.meta.When; import jakarta.annotation.Resource; -import org.junit.jupiter.api.Disabled; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -46,8 +46,6 @@ import org.springframework.core.annotation.AnnotationUtilsTests.WebMapping; import org.springframework.core.testfixture.stereotype.Component; import org.springframework.core.testfixture.stereotype.Indexed; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.Nullable; import org.springframework.util.MultiValueMap; import static java.util.Arrays.asList; @@ -55,7 +53,6 @@ import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.springframework.core.annotation.AnnotatedElementUtils.findAllMergedAnnotations; import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; import static org.springframework.core.annotation.AnnotatedElementUtils.getAllAnnotationAttributes; @@ -95,31 +92,18 @@ void getMergedAnnotationAttributesWithConventionBasedComposedAnnotation() { AnnotationAttributes attributes = getMergedAnnotationAttributes(element, name); assertThat(attributes).as("Should find @ContextConfig on " + element.getSimpleName()).isNotNull(); - assertThat(attributes.getStringArray("locations")).as("locations").containsExactly("explicitDeclaration"); - assertThat(attributes.getStringArray("value")).as("value").containsExactly("explicitDeclaration"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration". + assertThat(attributes.getStringArray("locations")).as("locations").isEmpty(); + assertThat(attributes.getStringArray("value")).as("value").isEmpty(); // Verify contracts between utility methods: assertThat(isAnnotated(element, name)).isTrue(); } - /** - * This test should never pass, simply because Spring does not support a hybrid - * approach for annotation attribute overrides with transitive implicit aliases. - * See SPR-13554 for details. - *

    Furthermore, if you choose to execute this test, it can fail for either - * the first test class or the second one (with different exceptions), depending - * on the order in which the JVM returns the attribute methods via reflection. - */ - @Disabled("Permanently disabled but left in place for illustrative purposes") @Test - void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation() { - for (Class clazz : asList(HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class, - HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class)) { - getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(clazz); - } - } - - private void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotation(Class clazz) { + void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotationV1() { + Class clazz = HalfConventionBasedAndHalfAliasedComposedContextConfigClassV1.class; String name = ContextConfig.class.getName(); String simpleName = clazz.getSimpleName(); AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name); @@ -135,18 +119,27 @@ private void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedC } @Test - void getMergedAnnotationAttributesWithInvalidConventionBasedComposedAnnotation() { - Class element = InvalidConventionBasedComposedContextConfigClass.class; - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - getMergedAnnotationAttributes(element, ContextConfig.class)) - .withMessageContaining("Different @AliasFor mirror values for annotation") - .withMessageContaining("attribute 'locations' and its alias 'value'") - .withMessageContaining("values of [{requiredLocationsDeclaration}] and [{duplicateDeclaration}]"); + void getMergedAnnotationAttributesWithHalfConventionBasedAndHalfAliasedComposedAnnotationV2() { + Class clazz = HalfConventionBasedAndHalfAliasedComposedContextConfigClassV2.class; + String name = ContextConfig.class.getName(); + String simpleName = clazz.getSimpleName(); + AnnotationAttributes attributes = getMergedAnnotationAttributes(clazz, name); + + assertThat(attributes).as("Should find @ContextConfig on " + simpleName).isNotNull(); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration". + assertThat(attributes.getStringArray("locations")).as("locations for class [" + simpleName + "]").isEmpty(); + assertThat(attributes.getStringArray("value")).as("value for class [" + simpleName + "]").isEmpty(); + + // Verify contracts between utility methods: + assertThat(isAnnotated(clazz, name)).isTrue(); } @Test void findMergedAnnotationAttributesWithSingleElementOverridingAnArrayViaConvention() { - assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "com.example.app.test". + assertComponentScanAttributes(ConventionBasedSinglePackageComponentScanClass.class); } @Test @@ -158,12 +151,16 @@ void findMergedAnnotationWithLocalAliasesThatConflictWithAttributesInMetaAnnotat assertThat(contextConfig.locations()).as("locations for " + element).isEmpty(); // 'value' in @SpringAppConfig should not override 'value' in @ContextConfig assertThat(contextConfig.value()).as("value for " + element).isEmpty(); - assertThat(contextConfig.classes()).as("classes for " + element).containsExactly(Number.class); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect Number.class. + assertThat(contextConfig.classes()).as("classes for " + element).isEmpty(); } @Test void findMergedAnnotationWithSingleElementOverridingAnArrayViaConvention() throws Exception { - assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "/test". + assertWebMapping(WebController.class.getMethod("postMappedWithPathAttribute"), ""); } } @@ -237,10 +234,11 @@ void isAnnotatedOnClassWithMetaDepth() { } @Test + @SuppressWarnings("deprecation") void isAnnotatedForPlainTypes() { assertThat(isAnnotated(Order.class, Documented.class)).isTrue(); - assertThat(isAnnotated(NonNullApi.class, Documented.class)).isTrue(); - assertThat(isAnnotated(NonNullApi.class, Nonnull.class)).isTrue(); + assertThat(isAnnotated(org.springframework.lang.NonNullApi.class, Documented.class)).isTrue(); + assertThat(isAnnotated(org.springframework.lang.NonNullApi.class, Nonnull.class)).isTrue(); assertThat(isAnnotated(ParametersAreNonnullByDefault.class, Nonnull.class)).isTrue(); } @@ -277,10 +275,11 @@ void hasAnnotationOnClassWithMetaDepth() { } @Test + @SuppressWarnings("deprecation") void hasAnnotationForPlainTypes() { assertThat(hasAnnotation(Order.class, Documented.class)).isTrue(); - assertThat(hasAnnotation(NonNullApi.class, Documented.class)).isTrue(); - assertThat(hasAnnotation(NonNullApi.class, Nonnull.class)).isTrue(); + assertThat(hasAnnotation(org.springframework.lang.NonNullApi.class, Documented.class)).isTrue(); + assertThat(hasAnnotation(org.springframework.lang.NonNullApi.class, Nonnull.class)).isTrue(); assertThat(hasAnnotation(ParametersAreNonnullByDefault.class, Nonnull.class)).isTrue(); } @@ -344,9 +343,10 @@ void getAllAnnotationAttributesOnClassWithMultipleComposedAnnotations() { } @Test + @SuppressWarnings("deprecation") void getAllAnnotationAttributesOnLangType() { MultiValueMap attributes = getAllAnnotationAttributes( - NonNullApi.class, Nonnull.class.getName()); + org.springframework.lang.NonNullApi.class, Nonnull.class.getName()); assertThat(attributes).as("Annotation attributes map for @Nonnull on NonNullApi").isNotNull(); assertThat(attributes.get("when")).as("value for NonNullApi").isEqualTo(List.of(When.ALWAYS)); } @@ -829,15 +829,15 @@ void findMergedAnnotationForMultipleMetaAnnotationsWithClashingAttributeNames() @Test void findMergedAnnotationWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); - assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); + assertWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"), "/test"); + assertWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"), "/test"); } - private void assertWebMapping(AnnotatedElement element) { + private void assertWebMapping(AnnotatedElement element, String expectedPath) { WebMapping webMapping = findMergedAnnotation(element, WebMapping.class); assertThat(webMapping).isNotNull(); - assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray("/test")); - assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray("/test")); + assertThat(webMapping.value()).as("value attribute: ").isEqualTo(asArray(expectedPath)); + assertThat(webMapping.path()).as("path attribute: ").isEqualTo(asArray(expectedPath)); } @Test @@ -1088,8 +1088,7 @@ static class MetaCycleAnnotatedClass { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedComposedContextConfig { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class) + // Do NOT use @AliasFor here String[] locations() default {}; } @@ -1097,8 +1096,7 @@ static class MetaCycleAnnotatedClass { @Retention(RetentionPolicy.RUNTIME) @interface InvalidConventionBasedComposedContextConfig { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class) + // Do NOT use @AliasFor here String[] locations(); } @@ -1256,13 +1254,11 @@ static class MetaCycleAnnotatedClass { @AliasFor(annotation = ContextConfig.class, attribute = "locations") String[] locations() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("value") Class[] classes() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfig.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("classes") Class[] value() default {}; } @@ -1301,8 +1297,7 @@ static class MetaCycleAnnotatedClass { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedSinglePackageComponentScan { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ComponentScan.class) + // Do NOT use @AliasFor here String basePackages(); } @@ -1548,15 +1543,13 @@ static class ResourceHolder { interface TransactionalService { @Transactional - @Nullable - Object doIt(); + @Nullable Object doIt(); } class TransactionalServiceImpl implements TransactionalService { @Override - @Nullable - public Object doIt() { + public @Nullable Object doIt() { return null; } } 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 9ca47d7f6351..434bfe7c9312 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 @@ 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/AnnotationFilterTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java index de5319b4f942..56695ac3fba3 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationFilterTests.java @@ -21,9 +21,10 @@ import javax.annotation.Nonnull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; +import org.springframework.lang.Contract; import org.springframework.util.ObjectUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -70,7 +71,7 @@ void plainWhenJavaLangAnnotationReturnsTrue() { @Test void plainWhenSpringLangAnnotationReturnsTrue() { - assertThat(AnnotationFilter.PLAIN.matches(Nullable.class)).isTrue(); + assertThat(AnnotationFilter.PLAIN.matches(Contract.class)).isTrue(); } @Test diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java index 16c5425116ea..5273042d428c 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationTypeMappingsTests.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. @@ -54,27 +54,24 @@ void forAnnotationTypeWhenNoMetaAnnotationsReturnsMappings() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(SimpleAnnotation.class); assertThat(mappings.size()).isEqualTo(1); assertThat(mappings.get(0).getAnnotationType()).isEqualTo(SimpleAnnotation.class); - assertThat(getAll(mappings)).flatExtracting( - AnnotationTypeMapping::getAnnotationType).containsExactly(SimpleAnnotation.class); + assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) + .containsExactly(SimpleAnnotation.class); } @Test void forAnnotationTypeWhenMetaAnnotationsReturnsMappings() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(MetaAnnotated.class); assertThat(mappings.size()).isEqualTo(6); - assertThat(getAll(mappings)).flatExtracting( - AnnotationTypeMapping::getAnnotationType).containsExactly( - MetaAnnotated.class, A.class, B.class, AA.class, AB.class, - ABC.class); + assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) + .containsExactly(MetaAnnotated.class, A.class, B.class, AA.class, AB.class, ABC.class); } @Test void forAnnotationTypeWhenHasRepeatingMetaAnnotationReturnsMapping() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(WithRepeatedMetaAnnotations.class); assertThat(mappings.size()).isEqualTo(3); - assertThat(getAll(mappings)).flatExtracting( - AnnotationTypeMapping::getAnnotationType).containsExactly( - WithRepeatedMetaAnnotations.class, Repeating.class, Repeating.class); + assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) + .containsExactly(WithRepeatedMetaAnnotations.class, Repeating.class, Repeating.class); } @Test @@ -89,56 +86,52 @@ void forAnnotationTypeWhenRepeatableMetaAnnotationIsFiltered() { void forAnnotationTypeWhenSelfAnnotatedReturnsMapping() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(SelfAnnotated.class); assertThat(mappings.size()).isEqualTo(1); - assertThat(getAll(mappings)).flatExtracting( - AnnotationTypeMapping::getAnnotationType).containsExactly(SelfAnnotated.class); + assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) + .containsExactly(SelfAnnotated.class); } @Test void forAnnotationTypeWhenFormsLoopReturnsMapping() { AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(LoopA.class); assertThat(mappings.size()).isEqualTo(2); - assertThat(getAll(mappings)).flatExtracting( - AnnotationTypeMapping::getAnnotationType).containsExactly(LoopA.class, LoopB.class); + assertThat(getAll(mappings)).flatExtracting(AnnotationTypeMapping::getAnnotationType) + .containsExactly(LoopA.class, LoopB.class); } @Test void forAnnotationTypeWhenHasAliasForWithBothValueAndAttributeThrowsException() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForWithBothValueAndAttribute.class)) - .withMessage("In @AliasFor declared on attribute 'test' in annotation [" - + AliasForWithBothValueAndAttribute.class.getName() - + "], attribute 'attribute' and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithBothValueAndAttribute.class)) + .withMessage("In @AliasFor declared on attribute 'test' in annotation [%s], attribute 'attribute' " + + "and its alias 'value' are present with values of 'foo' and 'bar', but only one is permitted.", + AliasForWithBothValueAndAttribute.class.getName()); } @Test void forAnnotationTypeWhenAliasForToSelfNonExistingAttribute() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForToSelfNonExistingAttribute.class)) - .withMessage("@AliasFor declaration on attribute 'test' in annotation [" - + AliasForToSelfNonExistingAttribute.class.getName() - + "] declares an alias for 'missing' which is not present."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfNonExistingAttribute.class)) + .withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] " + + "declares an alias for 'missing' which is not present.", + AliasForToSelfNonExistingAttribute.class.getName()); } @Test void forAnnotationTypeWhenAliasForToOtherNonExistingAttribute() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForToOtherNonExistingAttribute.class)) - .withMessage("Attribute 'test' in annotation [" - + AliasForToOtherNonExistingAttribute.class.getName() - + "] is declared as an @AliasFor nonexistent " - + "attribute 'missing' in annotation [" - + AliasForToOtherNonExistingAttributeTarget.class.getName() - + "]."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToOtherNonExistingAttribute.class)) + .withMessage("Attribute 'test' in annotation [%s] is declared as an @AliasFor nonexistent " + + "attribute 'missing' in annotation [%s].", AliasForToOtherNonExistingAttribute.class.getName(), + AliasForToOtherNonExistingAttributeTarget.class.getName()); } @Test void forAnnotationTypeWhenAliasForToSelf() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForToSelf.class)) - .withMessage("@AliasFor declaration on attribute 'test' in annotation [" - + AliasForToSelf.class.getName() - + "] points to itself. Specify 'annotation' to point to " - + "a same-named attribute on a meta-annotation."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelf.class)) + .withMessage("@AliasFor declaration on attribute 'test' in annotation [%s] points to itself. " + + "Specify 'annotation' to point to a same-named attribute on a meta-annotation.", + AliasForToSelf.class.getName()); } @Test @@ -152,13 +145,12 @@ void forAnnotationTypeWhenAliasForWithArrayCompatibleReturnTypes() { @Test void forAnnotationTypeWhenAliasForWithIncompatibleReturnTypes() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForWithIncompatibleReturnTypes.class)) - .withMessage("Misconfigured aliases: attribute 'test' in annotation [" - + AliasForWithIncompatibleReturnTypes.class.getName() - + "] and attribute 'test' in annotation [" - + AliasForWithIncompatibleReturnTypesTarget.class.getName() - + "] must declare the same return type."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForWithIncompatibleReturnTypes.class)) + .withMessage("Misconfigured aliases: attribute 'test' in annotation [%s] and attribute 'test' " + + "in annotation [%s] must declare the same return type.", + AliasForWithIncompatibleReturnTypes.class.getName(), + AliasForWithIncompatibleReturnTypesTarget.class.getName()); } @Test @@ -166,9 +158,8 @@ void forAnnotationTypeWhenAliasForToSelfAnnotatedToOtherAttribute() { String annotationType = AliasForToSelfAnnotatedToOtherAttribute.class.getName(); assertThatExceptionOfType(AnnotationConfigurationException.class) .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForToSelfAnnotatedToOtherAttribute.class)) - .withMessage("Attribute 'b' in annotation [" + annotationType - + "] must be declared as an @AliasFor attribute 'a' in annotation [" + annotationType - + "], not attribute 'c' in annotation [" + annotationType + "]."); + .withMessage("Attribute 'b' in annotation [%1$s] must be declared as an @AliasFor attribute 'a' in " + + "annotation [%1$s], not attribute 'c' in annotation [%1$s].", annotationType); } @Test @@ -182,53 +173,45 @@ private void assertMixedImplicitAndExplicitAliases(Class a String metaAnnotationName = AliasPair.class.getName(); assertThatExceptionOfType(AnnotationConfigurationException.class) .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(annotationType)) - .withMessage("Attribute 'b' in annotation [" + annotationName - + "] must be declared as an @AliasFor attribute 'a' in annotation [" + annotationName - + "], not attribute '" + overriddenAttribute + "' in annotation [" + metaAnnotationName + "]."); + .withMessage("Attribute 'b' in annotation [" + annotationName + + "] must be declared as an @AliasFor attribute 'a' in annotation [" + annotationName + + "], not attribute '" + overriddenAttribute + "' in annotation [" + metaAnnotationName + "]."); } @Test void forAnnotationTypeWhenAliasForNonMetaAnnotated() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForNonMetaAnnotated.class)) - .withMessage("@AliasFor declaration on attribute 'test' in annotation [" - + AliasForNonMetaAnnotated.class.getName() - + "] declares an alias for attribute 'test' in annotation [" - + AliasForNonMetaAnnotatedTarget.class.getName() - + "] which is not meta-present."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForNonMetaAnnotated.class)) + .withMessage("@AliasFor declaration on attribute 'test' in annotation [" + AliasForNonMetaAnnotated.class.getName() + + "] declares an alias for attribute 'test' in annotation [" + AliasForNonMetaAnnotatedTarget.class.getName() + + "] which is not meta-present."); } @Test void forAnnotationTypeWhenAliasForSelfWithDifferentDefaults() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForSelfWithDifferentDefaults.class)) - .withMessage("Misconfigured aliases: attribute 'a' in annotation [" - + AliasForSelfWithDifferentDefaults.class.getName() - + "] and attribute 'b' in annotation [" - + AliasForSelfWithDifferentDefaults.class.getName() - + "] must declare the same default value."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithDifferentDefaults.class)) + .withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() + + "] and attribute 'b' in annotation [" + AliasForSelfWithDifferentDefaults.class.getName() + + "] must declare the same default value."); } @Test void forAnnotationTypeWhenAliasForSelfWithMissingDefault() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasForSelfWithMissingDefault.class)) - .withMessage("Misconfigured aliases: attribute 'a' in annotation [" - + AliasForSelfWithMissingDefault.class.getName() - + "] and attribute 'b' in annotation [" - + AliasForSelfWithMissingDefault.class.getName() - + "] must declare default values."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasForSelfWithMissingDefault.class)) + .withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasForSelfWithMissingDefault.class.getName() + + "] and attribute 'b' in annotation [" + AliasForSelfWithMissingDefault.class.getName() + + "] must declare default values."); } @Test void forAnnotationTypeWhenAliasWithExplicitMirrorAndDifferentDefaults() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - AnnotationTypeMappings.forAnnotationType(AliasWithExplicitMirrorAndDifferentDefaults.class)) - .withMessage("Misconfigured aliases: attribute 'a' in annotation [" - + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() - + "] and attribute 'c' in annotation [" - + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() - + "] must declare the same default value."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> AnnotationTypeMappings.forAnnotationType(AliasWithExplicitMirrorAndDifferentDefaults.class)) + .withMessage("Misconfigured aliases: attribute 'a' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() + + "] and attribute 'c' in annotation [" + AliasWithExplicitMirrorAndDifferentDefaults.class.getName() + + "] must declare the same default value."); } @Test @@ -279,12 +262,6 @@ void getAliasMappingReturnsAttributes() throws Exception { assertThat(getAliasMapping(mapping, 0)).isEqualTo(Mapped.class.getDeclaredMethod("alias")); } - @Test - void getConventionMappingReturnsAttributes() throws Exception { - AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(Mapped.class).get(1); - assertThat(getConventionMapping(mapping, 1)).isEqualTo(Mapped.class.getDeclaredMethod("convention")); - } - @Test void getMirrorSetWhenAliasPairReturnsMirrors() { AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); @@ -375,18 +352,16 @@ void resolveMirrorsWhenOnlyHasDefaultValuesUsesFirst() { @Test void resolveMirrorsWhenHasDifferentValuesThrowsException() { AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(AliasPair.class).get(0); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, AliasPair.class)) - .withMessage("Different @AliasFor mirror values for annotation [" - + AliasPair.class.getName() + "] declared on " - + WithDifferentValueAliasPair.class.getName() - + "; attribute 'a' and its alias 'b' are declared with values of [test1] and [test2]."); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> resolveMirrorSets(mapping, WithDifferentValueAliasPair.class, AliasPair.class)) + .withMessage("Different @AliasFor mirror values for annotation [" + AliasPair.class.getName() + "] declared on " + + WithDifferentValueAliasPair.class.getName() + + "; attribute 'a' and its alias 'b' are declared with values of [test1] and [test2]."); } @Test void resolveMirrorsWhenHasWithMultipleRoutesToAliasReturnsMirrors() { - AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( - MultipleRoutesToAliasA.class); + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(MultipleRoutesToAliasA.class); AnnotationTypeMapping mappingsA = getMapping(mappings, MultipleRoutesToAliasA.class); assertThat(mappingsA.getMirrorSets().size()).isZero(); AnnotationTypeMapping mappingsB = getMapping(mappings, MultipleRoutesToAliasB.class); @@ -397,8 +372,7 @@ void resolveMirrorsWhenHasWithMultipleRoutesToAliasReturnsMirrors() { @Test void getAliasMappingWhenHasWithMultipleRoutesToAliasReturnsMappedAttributes() { - AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType( - MultipleRoutesToAliasA.class); + AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(MultipleRoutesToAliasA.class); AnnotationTypeMapping mappingsA = getMapping(mappings, MultipleRoutesToAliasA.class); assertThat(getAliasMapping(mappingsA, 0)).isNull(); AnnotationTypeMapping mappingsB = getMapping(mappings, MultipleRoutesToAliasB.class); @@ -410,14 +384,6 @@ void getAliasMappingWhenHasWithMultipleRoutesToAliasReturnsMappedAttributes() { assertThat(getAliasMapping(mappingsC, 1).getName()).isEqualTo("a1"); } - @Test - void getConventionMappingWhenConventionToExplicitAliasesReturnsMappedAttributes() { - AnnotationTypeMappings mappings = AnnotationTypeMappings.forAnnotationType(ConventionToExplicitAliases.class); - AnnotationTypeMapping mapping = getMapping(mappings, ConventionToExplicitAliasesTarget.class); - assertThat(mapping.getConventionMapping(0)).isEqualTo(0); - assertThat(mapping.getConventionMapping(1)).isEqualTo(0); - } - @Test void isEquivalentToDefaultValueWhenValueAndDefaultAreNullReturnsTrue() { AnnotationTypeMapping mapping = AnnotationTypeMappings.forAnnotationType(ClassValue.class).get(0); @@ -476,18 +442,11 @@ private Method[] resolveMirrorSets(AnnotationTypeMapping mapping, Class eleme return result; } - @Nullable - private Method getAliasMapping(AnnotationTypeMapping mapping, int attributeIndex) { + private @Nullable Method getAliasMapping(AnnotationTypeMapping mapping, int attributeIndex) { int mapped = mapping.getAliasMapping(attributeIndex); return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; } - @Nullable - private Method getConventionMapping(AnnotationTypeMapping mapping, int attributeIndex) { - int mapped = mapping.getConventionMapping(attributeIndex); - return mapped != -1 ? mapping.getRoot().getAttributes().get(mapped) : null; - } - private AnnotationTypeMapping getMapping(AnnotationTypeMappings mappings, Class annotationType) { @@ -892,23 +851,6 @@ static class WithDefaultValueAliasPair { String a1() default ""; } - @Retention(RetentionPolicy.RUNTIME) - @interface ConventionToExplicitAliasesTarget { - - @AliasFor("test") - String value() default ""; - - @AliasFor("value") - String test() default ""; - } - - @Retention(RetentionPolicy.RUNTIME) - @ConventionToExplicitAliasesTarget - @interface ConventionToExplicitAliases { - - String test() default ""; - } - @Retention(RetentionPolicy.RUNTIME) @interface ClassValue { diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java index 4185df3c4a13..bac3c96eb09b 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationUtilsTests.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. @@ -40,7 +40,6 @@ import org.springframework.core.annotation.subpackage.NonPublicAnnotatedClass; import org.springframework.core.testfixture.ide.IdeUtils; import org.springframework.core.testfixture.stereotype.Component; -import org.springframework.lang.NonNullApi; import static java.util.Arrays.asList; import static java.util.Arrays.stream; @@ -429,8 +428,7 @@ void isAnnotationInheritedForAllScenarios() { @Test void isAnnotationMetaPresentForPlainType() { assertThat(isAnnotationMetaPresent(Order.class, Documented.class)).isTrue(); - assertThat(isAnnotationMetaPresent(NonNullApi.class, Documented.class)).isTrue(); - assertThat(isAnnotationMetaPresent(NonNullApi.class, Nonnull.class)).isTrue(); + assertThat(isAnnotationMetaPresent(ParametersAreNonnullByDefault.class, Documented.class)).isTrue(); assertThat(isAnnotationMetaPresent(ParametersAreNonnullByDefault.class, Nonnull.class)).isTrue(); } @@ -925,8 +923,8 @@ void synthesizeAnnotationFromMapWithAttributeOfIncorrectType() { Map map = Collections.singletonMap(VALUE, 42L); assertThatIllegalStateException().isThrownBy(() -> synthesizeAnnotation(map, Component.class, null).value()) - .withMessageContaining("Attribute 'value' in annotation org.springframework.core.testfixture.stereotype.Component " - + "should be compatible with java.lang.String but a java.lang.Long value was returned"); + .withMessageContaining("Attribute 'value' in annotation org.springframework.core.testfixture.stereotype.Component " + + "should be compatible with java.lang.String but a java.lang.Long value was returned"); } @Test @@ -1358,14 +1356,15 @@ enum RequestMethod { /** * Mock of {@code org.springframework.web.bind.annotation.PostMapping}, except - * that the path is overridden by convention with single String element. + * that the path is intended to be overridden by convention with single String + * element. However, convention-based annotation attribute overrides are no + * longer supported as of Spring Framework 7.0. */ @Retention(RetentionPolicy.RUNTIME) @WebMapping(method = RequestMethod.POST, name = "") @interface Post { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = WebMapping.class) + // Do NOT use @AliasFor here String path() default ""; } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java index 6400a880c33c..d5bbde3f5f87 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/AnnotationsScannerTests.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. @@ -29,11 +29,12 @@ import java.util.function.Predicate; import java.util.stream.Stream; +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.MergedAnnotations.Search; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -469,15 +470,13 @@ void scanWhenProcessorReturnsFromDoWithAggregateExitsEarly() { new AnnotationsProcessor() { @Override - @Nullable - public String doWithAggregate(Object context, int aggregateIndex) { + public @NonNull String doWithAggregate(Object context, int aggregateIndex) { return ""; } @Override - @Nullable - public String doWithAnnotations(Object context, int aggregateIndex, - Object source, Annotation[] annotations) { + public @NonNull String doWithAnnotations(Object context, int aggregateIndex, + @Nullable Object source, @Nullable Annotation @Nullable [] annotations) { throw new IllegalStateException("Should not call"); } @@ -503,15 +502,13 @@ void scanWhenProcessorHasFinishMethodUsesFinishResult() { new AnnotationsProcessor() { @Override - @Nullable - public String doWithAnnotations(Object context, int aggregateIndex, - Object source, Annotation[] annotations) { + public @NonNull String doWithAnnotations(Object context, int aggregateIndex, + @Nullable Object source, @Nullable Annotation @Nullable [] annotations) { return "K"; } @Override - @Nullable - public String finish(String result) { + public @NonNull String finish(@Nullable String result) { return "O" + result; } @@ -793,13 +790,11 @@ public void method() { interface IgnorableOverrideInterface1 { - @Nullable void method(); } interface IgnorableOverrideInterface2 { - @Nullable void method(); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java index d69cc41e7c10..9073adaa336c 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationClassLoaderTests.java @@ -120,9 +120,7 @@ public TestClassLoader(ClassLoader parent) { @Override protected boolean isEligibleForOverriding(String className) { - return WITH_TEST_ANNOTATION.equals(className) - || TEST_ANNOTATION.equals(className) - || TEST_REFERENCE.equals(className); + return WITH_TEST_ANNOTATION.equals(className) || TEST_ANNOTATION.equals(className) || TEST_REFERENCE.equals(className); } } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java index a0bbaa5b234a..6cc0fb64f811 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsCollectionTests.java @@ -171,8 +171,7 @@ void getWithPredicateReturnsOnlyMatching() { void getWithSelectorReturnsSelected() { MergedAnnotations annotations = getMultiRoute1(); MergedAnnotationSelector deepest = (existing, - candidate) -> candidate.getDistance() > existing.getDistance() ? candidate - : existing; + candidate) -> candidate.getDistance() > existing.getDistance() ? candidate : existing; assertThat(annotations.get(MultiRouteTarget.class, null, deepest).getString( MergedAnnotation.VALUE)).isEqualTo("111"); } diff --git a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java index 404073b9b42b..a734c51a47cb 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/MergedAnnotationsRepeatableAnnotationTests.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. @@ -274,7 +274,7 @@ private Set getAnnotations(Class private Set getAnnotations(Class container, Class repeatable, SearchStrategy searchStrategy, AnnotatedElement element, AnnotationFilter annotationFilter) { - RepeatableContainers containers = RepeatableContainers.of(repeatable, container); + RepeatableContainers containers = RepeatableContainers.explicitRepeatable(repeatable, container); MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, containers, annotationFilter); return annotations.stream(repeatable).collect(MergedAnnotationCollectors.toAnnotationSet()); } 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 093b570a53c4..7733a9c641f6 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 @@ -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 java.util.stream.Stream; import jakarta.annotation.Resource; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -49,7 +50,6 @@ import org.springframework.core.testfixture.ide.IdeUtils; import org.springframework.core.testfixture.stereotype.Component; import org.springframework.core.testfixture.stereotype.Indexed; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.MultiValueMap; import org.springframework.util.ReflectionUtils; @@ -63,8 +63,8 @@ /** * Tests for {@link MergedAnnotations} and {@link MergedAnnotation}. These tests - * cover common usage scenarios and were mainly ported from the original - * {@code AnnotationUtils} and {@code AnnotatedElementUtils} tests. + * cover common usage scenarios and were mainly ported from the original tests in + * {@link AnnotationUtilsTests} and {@link AnnotatedElementUtilsTests}. * * @author Phillip Webb * @author Rod Johnson @@ -136,7 +136,7 @@ void searchFromClassWithCustomAnnotationFilter() { @Test void searchFromClassWithCustomRepeatableContainers() { assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty(); - RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, Hierarchy.class); + RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class); MergedAnnotations annotations = MergedAnnotations.search(SearchStrategy.DIRECT) .withRepeatableContainers(containers) @@ -218,8 +218,10 @@ void getWithInheritedAnnotationsAttributesWithConventionBasedComposedAnnotation( MergedAnnotations.from(ConventionBasedComposedContextConfigurationClass.class, SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class); assertThat(annotation.isPresent()).isTrue(); - assertThat(annotation.getStringArray("locations")).containsExactly("explicitDeclaration"); - assertThat(annotation.getStringArray("value")).containsExactly("explicitDeclaration"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "explicitDeclaration". + assertThat(annotation.getStringArray("locations")).isEmpty(); + assertThat(annotation.getStringArray("value")).isEmpty(); } @Test @@ -244,16 +246,11 @@ void getWithInheritedAnnotationsFromHalfConventionBasedAndHalfAliasedComposedAnn assertThat(annotation.getStringArray("value")).isEmpty(); } - @Test - void getWithInheritedAnnotationsFromInvalidConventionBasedComposedAnnotation() { - assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> MergedAnnotations.from(InvalidConventionBasedComposedContextConfigurationClass.class, - SearchStrategy.INHERITED_ANNOTATIONS).get(ContextConfiguration.class)); - } - @Test void getWithTypeHierarchyWithSingleElementOverridingAnArrayViaConvention() { - testGetWithTypeHierarchy(ConventionBasedSinglePackageComponentScanClass.class, "com.example.app.test"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "com.example.app.test". + testGetWithTypeHierarchy(ConventionBasedSinglePackageComponentScanClass.class); } @Test @@ -263,12 +260,16 @@ void getWithTypeHierarchyWithLocalAliasesThatConflictWithAttributesInMetaAnnotat .get(ContextConfiguration.class); assertThat(annotation.getStringArray("locations")).isEmpty(); assertThat(annotation.getStringArray("value")).isEmpty(); - assertThat(annotation.getClassArray("classes")).containsExactly(Number.class); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect Number.class. + assertThat(annotation.getClassArray("classes")).isEmpty(); } @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaConvention() throws Exception { - testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute")); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "/test". + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("postMappedWithPathAttribute"), ""); } } @@ -781,15 +782,15 @@ void getWithTypeHierarchyWhenMultipleMetaAnnotationsHaveClashingAttributeNames() @Test void getWithTypeHierarchyOnMethodWithSingleElementOverridingAnArrayViaAliasFor() throws Exception { - testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute")); - testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute")); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithValueAttribute"), "/test"); + testGetWithTypeHierarchyWebMapping(WebController.class.getMethod("getMappedWithPathAttribute"), "/test"); } - private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element) { + private void testGetWithTypeHierarchyWebMapping(AnnotatedElement element, String expectedPath) { MergedAnnotation annotation = MergedAnnotations.from(element, SearchStrategy.TYPE_HIERARCHY) .get(RequestMapping.class); - assertThat(annotation.getStringArray("value")).containsExactly("/test"); - assertThat(annotation.getStringArray("path")).containsExactly("/test"); + assertThat(annotation.getStringArray("value")).containsExactly(expectedPath); + assertThat(annotation.getStringArray("path")).containsExactly(expectedPath); } @Test @@ -983,7 +984,7 @@ void getDirectFromClassFavorsMoreLocallyDeclaredComposedAnnotationsOverInherited } @Test - void getDirectFromClassgetDirectFromClassMetaMetaAnnotatedClass() { + void getDirectFromClassWithMetaMetaAnnotatedClass() { MergedAnnotation annotation = MergedAnnotations.from( MetaMetaAnnotatedClass.class, SearchStrategy.TYPE_HIERARCHY).get(Component.class); assertThat(annotation.getString("value")).isEqualTo("meta2"); @@ -1299,8 +1300,8 @@ void getDirectWithAttributeAliases2() throws Exception { @Test void getDirectWithAttributeAliasesWithDifferentValues() throws Exception { Method method = WebController.class.getMethod("handleMappedWithDifferentPathAndValueAttributes"); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotations.from(method).get(RequestMapping.class)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotations.from(method).get(RequestMapping.class)) .withMessageContaining("attribute 'path' and its alias 'value'") .withMessageContaining("values of [{/test}] and [{/enigma}]"); } @@ -1362,16 +1363,16 @@ void streamRepeatableDeclaredOnMethod() throws Exception { @Test @SuppressWarnings("deprecation") void streamRepeatableDeclaredOnClassWithAttributeAliases() { - assertThat(MergedAnnotations.from(HierarchyClass.class).stream( - TestConfiguration.class)).isEmpty(); - RepeatableContainers containers = RepeatableContainers.of(TestConfiguration.class, - Hierarchy.class); + assertThat(MergedAnnotations.from(HierarchyClass.class).stream(TestConfiguration.class)).isEmpty(); + RepeatableContainers containers = RepeatableContainers.explicitRepeatable(TestConfiguration.class, Hierarchy.class); MergedAnnotations annotations = MergedAnnotations.from(HierarchyClass.class, SearchStrategy.DIRECT, containers, AnnotationFilter.NONE); - assertThat(annotations.stream(TestConfiguration.class).map( - annotation -> annotation.getString("location"))).containsExactly("A", "B"); - assertThat(annotations.stream(TestConfiguration.class).map( - annotation -> annotation.getString("value"))).containsExactly("A", "B"); + assertThat(annotations.stream(TestConfiguration.class) + .map(annotation -> annotation.getString("location"))) + .containsExactly("A", "B"); + assertThat(annotations.stream(TestConfiguration.class) + .map(annotation -> annotation.getString("value"))) + .containsExactly("A", "B"); } @Test @@ -1434,14 +1435,12 @@ private void testJavaRepeatables(SearchStrategy searchStrategy, Class element MyRepeatable[] annotations = searchStrategy == SearchStrategy.DIRECT ? element.getDeclaredAnnotationsByType(MyRepeatable.class) : element.getAnnotationsByType(MyRepeatable.class); - assertThat(Arrays.stream(annotations).map(MyRepeatable::value)).containsExactly( - expected); + assertThat(annotations).extracting(MyRepeatable::value).containsExactly(expected); } private void testExplicitRepeatables(SearchStrategy searchStrategy, Class element, String[] expected) { MergedAnnotations annotations = MergedAnnotations.from(element, searchStrategy, - RepeatableContainers.of(MyRepeatable.class, MyRepeatableContainer.class), - AnnotationFilter.PLAIN); + RepeatableContainers.explicitRepeatable(MyRepeatable.class, MyRepeatableContainer.class)); Stream values = annotations.stream(MyRepeatable.class) .filter(MergedAnnotationPredicates.firstRunOf(MergedAnnotation::getAggregateIndex)) .map(annotation -> annotation.getString("value")); @@ -1593,8 +1592,8 @@ void synthesizeWhenAliasForIsMissingAttributeDeclaration() { AliasForWithMissingAttributeDeclarationClass.class.getAnnotation( AliasForWithMissingAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(annotation)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") .withMessageContaining(AliasForWithMissingAttributeDeclaration.class.getName()) .withMessageContaining("points to itself"); @@ -1606,8 +1605,8 @@ void synthesizeWhenAliasForHasDuplicateAttributeDeclaration() { AliasForWithDuplicateAttributeDeclarationClass.class.getAnnotation( AliasForWithDuplicateAttributeDeclaration.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(annotation)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) .withMessageStartingWith("In @AliasFor declared on attribute 'foo' in annotation") .withMessageContaining(AliasForWithDuplicateAttributeDeclaration.class.getName()) .withMessageContaining("attribute 'attribute' and its alias 'value' are present with values of 'baz' and 'bar'"); @@ -1618,8 +1617,8 @@ void synthesizeWhenAttributeAliasForNonexistentAttribute() { AliasForNonexistentAttribute annotation = AliasForNonexistentAttributeClass.class.getAnnotation( AliasForNonexistentAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(annotation)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) .withMessageStartingWith("@AliasFor declaration on attribute 'foo' in annotation") .withMessageContaining(AliasForNonexistentAttribute.class.getName()) .withMessageContaining("declares an alias for 'bar' which is not present"); @@ -1631,8 +1630,8 @@ void synthesizeWhenAttributeAliasWithMirroredAliasForWrongAttribute() { AliasForWithMirroredAliasForWrongAttributeClass.class.getAnnotation( AliasForWithMirroredAliasForWrongAttribute.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(annotation)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) .withMessage("@AliasFor declaration on attribute 'bar' in annotation [" + AliasForWithMirroredAliasForWrongAttribute.class.getName() + "] declares an alias for 'quux' which is not present."); @@ -1687,8 +1686,8 @@ void synthesizeWhenAttributeAliasForMetaAnnotationThatIsNotMetaPresent() { AliasedComposedTestConfigurationNotMetaPresentClass.class.getAnnotation( AliasedComposedTestConfigurationNotMetaPresent.class); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(annotation)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(annotation)) .withMessageStartingWith("@AliasFor declaration on attribute 'xmlConfigFile' in annotation") .withMessageContaining(AliasedComposedTestConfigurationNotMetaPresent.class.getName()) .withMessageContaining("declares an alias for attribute 'location' in annotation") @@ -1780,8 +1779,8 @@ void synthesizeWithImplicitAliasesWithMissingDefaultValues() { ImplicitAliasesWithMissingDefaultValuesTestConfiguration.class; ImplicitAliasesWithMissingDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(clazz, config)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(clazz, config)) .withMessageStartingWith("Misconfigured aliases:") .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") @@ -1795,8 +1794,8 @@ void synthesizeWithImplicitAliasesWithDifferentDefaultValues() { ImplicitAliasesWithDifferentDefaultValuesTestConfiguration.class; ImplicitAliasesWithDifferentDefaultValuesTestConfiguration config = clazz.getAnnotation(annotationType); - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy( - () -> MergedAnnotation.from(clazz, config)) + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(clazz, config)) .withMessageStartingWith("Misconfigured aliases:") .withMessageContaining("attribute 'location1' in annotation [" + annotationType.getName() + "]") .withMessageContaining("attribute 'location2' in annotation [" + annotationType.getName() + "]") @@ -1892,8 +1891,8 @@ void synthesizeFromDefaultsWithAttributeAliases() { @Test void synthesizeWhenAttributeAliasesWithDifferentValues() { - assertThatExceptionOfType(AnnotationConfigurationException.class).isThrownBy(() -> - MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation(TestConfiguration.class)).synthesize()); + assertThatExceptionOfType(AnnotationConfigurationException.class) + .isThrownBy(() -> MergedAnnotation.from(TestConfigurationMismatch.class.getAnnotation(TestConfiguration.class))); } @Test @@ -1957,8 +1956,8 @@ void synthesizeFromMapWithNullAttributeValue() { } private void testMissingTextAttribute(Map attributes) { - assertThatExceptionOfType(NoSuchElementException.class).isThrownBy(() -> - MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) + assertThatExceptionOfType(NoSuchElementException.class) + .isThrownBy(() -> MergedAnnotation.of(AnnotationWithoutDefaults.class, attributes).synthesize().text()) .withMessage("No value found for attribute named 'text' in merged annotation " + AnnotationWithoutDefaults.class.getCanonicalName()); } @@ -1967,7 +1966,8 @@ private void testMissingTextAttribute(Map attributes) { void synthesizeFromMapWithAttributeOfIncorrectType() { Map map = Collections.singletonMap("value", 42L); MergedAnnotation annotation = MergedAnnotation.of(Component.class, map); - assertThatIllegalStateException().isThrownBy(() -> annotation.synthesize().value()) + assertThatIllegalStateException() + .isThrownBy(() -> annotation.synthesize().value()) .withMessage("Attribute 'value' in annotation " + "org.springframework.core.testfixture.stereotype.Component should be " + "compatible with java.lang.String but a java.lang.Long value was returned"); @@ -2160,9 +2160,11 @@ void synthesizeWithArrayOfChars() { @Test void getValueWhenHasDefaultOverride() { - MergedAnnotation annotation = MergedAnnotations.from(DefaultOverrideClass.class) - .get(DefaultOverrideRoot.class); - assertThat(annotation.getString("text")).isEqualTo("metameta"); + MergedAnnotation annotation = + MergedAnnotations.from(DefaultOverrideClass.class).get(DefaultOverrideRoot.class); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "metameta". + assertThat(annotation.getString("text")).isEqualTo("root"); } @Test // gh-22654 @@ -2370,24 +2372,13 @@ static class MetaMetaAliasedTransactionalClass { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedComposedContextConfiguration { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) + // Do NOT use @AliasFor here String[] locations() default {}; - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) + // Do NOT use @AliasFor here Class[] classes() default {}; } - @ContextConfiguration(value = "duplicateDeclaration") - @Retention(RetentionPolicy.RUNTIME) - @interface InvalidConventionBasedComposedContextConfiguration { - - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) - String[] locations(); - } - /** * This hybrid approach for annotation attribute overrides with transitive implicit * aliases is unsupported. See SPR-13554 for details. @@ -2396,8 +2387,7 @@ static class MetaMetaAliasedTransactionalClass { @Retention(RetentionPolicy.RUNTIME) @interface HalfConventionBasedAndHalfAliasedComposedContextConfiguration { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class) + // Do NOT use @AliasFor here String[] locations() default {}; @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") @@ -2519,13 +2509,11 @@ static class MetaMetaAliasedTransactionalClass { @AliasFor(annotation = ContextConfiguration.class, attribute = "locations") String[] locations() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("value") Class[] classes() default {}; - // Do NOT use @AliasFor(annotation = ...) here until Spring 6.1 - // @AliasFor(annotation = ContextConfiguration.class, attribute = "classes") + // Do NOT use @AliasFor(annotation = ...) @AliasFor("classes") Class[] value() default {}; } @@ -2562,8 +2550,7 @@ static class MetaMetaAliasedTransactionalClass { @Retention(RetentionPolicy.RUNTIME) @interface ConventionBasedSinglePackageComponentScan { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ComponentScan.class) + // Do NOT use @AliasFor here String basePackages(); } @@ -2698,10 +2685,6 @@ public interface SubSubNonInheritedAnnotationInterface static class ConventionBasedComposedContextConfigurationClass { } - @InvalidConventionBasedComposedContextConfiguration(locations = "requiredLocationsDeclaration") - static class InvalidConventionBasedComposedContextConfigurationClass { - } - @HalfConventionBasedAndHalfAliasedComposedContextConfiguration(xmlConfigFiles = "explicitDeclaration") static class HalfConventionBasedAndHalfAliasedComposedContextConfigurationClass1 { } @@ -2875,8 +2858,7 @@ interface AnnotatedInterface { interface NullableAnnotatedInterface { - @Nullable - void fromInterfaceImplementedByRoot(); + @Nullable String fromInterfaceImplementedByRoot(); } static class Root implements AnnotatedInterface { @@ -3154,8 +3136,7 @@ public String toString() { @RequestMapping(method = RequestMethod.POST, name = "") @interface PostMapping { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = RequestMapping.class) + // Do NOT use @AliasFor here String path() default ""; } @@ -3646,13 +3627,11 @@ interface TestConfigurationMismatch { @interface DefaultOverrideRoot { String text() default "root"; - } @Retention(RetentionPolicy.RUNTIME) @DefaultOverrideRoot @interface DefaultOverrideMeta { - } @Retention(RetentionPolicy.RUNTIME) @@ -3660,18 +3639,15 @@ interface TestConfigurationMismatch { @interface DefaultOverrideMetaMeta { String text() default "metameta"; - } @Retention(RetentionPolicy.RUNTIME) @DefaultOverrideMetaMeta @interface DefaultOverrideMetaMetaMeta { - } @DefaultOverrideMetaMetaMeta static class DefaultOverrideClass { - } @Retention(RetentionPolicy.RUNTIME) diff --git a/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java b/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java index 6e20b5973615..274f360ba4dd 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/NestedRepeatableAnnotationsTests.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,7 +109,7 @@ void streamRepeatableAnnotationsWithStandardRepeatables_MergedAnnotationsApi() { @Test void streamRepeatableAnnotationsWithExplicitRepeatables_MergedAnnotationsApi() { RepeatableContainers repeatableContainers = - RepeatableContainers.of(A.class, A.Container.class).and(B.Container.class, B.class); + RepeatableContainers.explicitRepeatable(A.class, A.Container.class).plus(B.class, B.Container.class); Set annotations = MergedAnnotations.from(method, SearchStrategy.TYPE_HIERARCHY, repeatableContainers) .stream(A.class).collect(MergedAnnotationCollectors.toAnnotationSet()); // Merged, so we expect to find @A twice with values coming from @B(5) and @B(10). @@ -127,8 +127,8 @@ void findMergedRepeatableAnnotationsWithStandardRepeatables_AnnotatedElementUtil void findMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() { Set annotations = AnnotatedElementUtils.findMergedRepeatableAnnotations(method, A.class, A.Container.class); // When findMergedRepeatableAnnotations(...) is invoked with an explicit container - // type, it uses RepeatableContainers.of(...) which limits the repeatable annotation - // support to a single container type. + // type, it uses RepeatableContainers.explicitRepeatable(...) which limits the + // repeatable annotation support to a single container type. // // In this test case, we are therefore limiting the support to @A.Container, which // means that @B.Container is unsupported and effectively ignored as a repeatable @@ -149,8 +149,8 @@ void getMergedRepeatableAnnotationsWithStandardRepeatables_AnnotatedElementUtils void getMergedRepeatableAnnotationsWithExplicitContainer_AnnotatedElementUtils() { Set annotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(method, A.class, A.Container.class); // When getMergedRepeatableAnnotations(...) is invoked with an explicit container - // type, it uses RepeatableContainers.of(...) which limits the repeatable annotation - // support to a single container type. + // type, it uses RepeatableContainers.explicitRepeatable(...) which limits the + // repeatable annotation support to a single container type. // // In this test case, we are therefore limiting the support to @A.Container, which // means that @B.Container is unsupported and effectively ignored as a repeatable diff --git a/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java index ee1f88ee5993..c28fcff1f8a8 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/RepeatableContainersTests.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. @@ -87,7 +87,7 @@ class ExplicitRepeatableContainerTests { @Test void ofExplicitWhenNonRepeatableReturnsNull() { Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), + RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class), NonRepeatableTestCase.class, NonRepeatable.class); assertThat(values).isNull(); } @@ -95,7 +95,7 @@ void ofExplicitWhenNonRepeatableReturnsNull() { @Test void ofExplicitWhenStandardRepeatableContainerReturnsNull() { Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), + RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class), StandardRepeatablesTestCase.class, StandardContainer.class); assertThat(values).isNull(); } @@ -103,14 +103,14 @@ void ofExplicitWhenStandardRepeatableContainerReturnsNull() { @Test void ofExplicitWhenContainerReturnsRepeats() { Object[] values = findRepeatedAnnotationValues( - RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class), + RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class), ExplicitRepeatablesTestCase.class, ExplicitContainer.class); assertThat(values).containsExactly("a", "b"); } @Test void ofExplicitWhenContainerIsNullDeducesContainer() { - Object[] values = findRepeatedAnnotationValues(RepeatableContainers.of(StandardRepeatable.class, null), + Object[] values = findRepeatedAnnotationValues(RepeatableContainers.explicitRepeatable(StandardRepeatable.class, null), StandardRepeatablesTestCase.class, StandardContainer.class); assertThat(values).containsExactly("a", "b"); } @@ -118,7 +118,7 @@ void ofExplicitWhenContainerIsNullDeducesContainer() { @Test void ofExplicitWhenHasNoValueThrowsException() { assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidNoValue.class)) + .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidNoValue.class)) .withMessageContaining("Invalid declaration of container type [%s] for repeatable annotation [%s]", InvalidNoValue.class.getName(), ExplicitRepeatable.class.getName()); } @@ -126,7 +126,7 @@ void ofExplicitWhenHasNoValueThrowsException() { @Test void ofExplicitWhenValueIsNotArrayThrowsException() { assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidNotArray.class)) + .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidNotArray.class)) .withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]", InvalidNotArray.class.getName(), ExplicitRepeatable.class.getName()); } @@ -134,7 +134,7 @@ void ofExplicitWhenValueIsNotArrayThrowsException() { @Test void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { assertThatExceptionOfType(AnnotationConfigurationException.class) - .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, InvalidWrongArrayType.class)) + .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, InvalidWrongArrayType.class)) .withMessage("Container type [%s] must declare a 'value' attribute for an array of type [%s]", InvalidWrongArrayType.class.getName(), ExplicitRepeatable.class.getName()); } @@ -142,14 +142,14 @@ void ofExplicitWhenValueIsArrayOfWrongTypeThrowsException() { @Test void ofExplicitWhenAnnotationIsNullThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> RepeatableContainers.of(null, null)) + .isThrownBy(() -> RepeatableContainers.explicitRepeatable(null, null)) .withMessage("Repeatable must not be null"); } @Test void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { assertThatIllegalArgumentException() - .isThrownBy(() -> RepeatableContainers.of(ExplicitRepeatable.class, null)) + .isThrownBy(() -> RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, null)) .withMessage("Annotation type must be a repeatable annotation: failed to resolve container type for %s", ExplicitRepeatable.class.getName()); } @@ -159,11 +159,11 @@ void ofExplicitWhenContainerIsNullAndNotRepeatableThrowsException() { @Test void standardAndExplicitReturnsRepeats() { RepeatableContainers repeatableContainers = RepeatableContainers.standardRepeatables() - .and(ExplicitContainer.class, ExplicitRepeatable.class); + .plus(ExplicitRepeatable.class, ExplicitContainer.class); assertThat(findRepeatedAnnotationValues(repeatableContainers, StandardRepeatablesTestCase.class, StandardContainer.class)) - .containsExactly("a", "b"); + .containsExactly("a", "b"); assertThat(findRepeatedAnnotationValues(repeatableContainers, ExplicitRepeatablesTestCase.class, ExplicitContainer.class)) - .containsExactly("a", "b"); + .containsExactly("a", "b"); } @Test @@ -175,10 +175,10 @@ void noneAlwaysReturnsNull() { @Test void equalsAndHashcode() { - RepeatableContainers c1 = RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class); - RepeatableContainers c2 = RepeatableContainers.of(ExplicitRepeatable.class, ExplicitContainer.class); + RepeatableContainers c1 = RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class); + RepeatableContainers c2 = RepeatableContainers.explicitRepeatable(ExplicitRepeatable.class, ExplicitContainer.class); RepeatableContainers c3 = RepeatableContainers.standardRepeatables(); - RepeatableContainers c4 = RepeatableContainers.standardRepeatables().and(ExplicitContainer.class, ExplicitRepeatable.class); + RepeatableContainers c4 = RepeatableContainers.standardRepeatables().plus(ExplicitRepeatable.class, ExplicitContainer.class); assertThat(c1).hasSameHashCodeAs(c2); assertThat(c1).isEqualTo(c1).isEqualTo(c2); assertThat(c1).isNotEqualTo(c3).isNotEqualTo(c4); diff --git a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java index d04d16a015f1..7e99dcceb2e2 100644 --- a/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java +++ b/spring-core/src/test/java/org/springframework/core/annotation/TypeMappedAnnotationTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,6 +33,7 @@ * for a much more extensive collection of tests. * * @author Phillip Webb + * @author Sam Brannen */ class TypeMappedAnnotationTests { @@ -62,11 +63,19 @@ void mappingExplicitAliasToMetaAnnotationReturnsMappedValues() { @Test void mappingConventionAliasToMetaAnnotationReturnsMappedValues() { TypeMappedAnnotation annotation = getTypeMappedAnnotation( + WithConventionAliasToMetaAnnotation.class, + ConventionAliasToMetaAnnotation.class); + assertThat(annotation.getString("value")).isEqualTo("value"); + assertThat(annotation.getString("convention")).isEqualTo("convention"); + + annotation = getTypeMappedAnnotation( WithConventionAliasToMetaAnnotation.class, ConventionAliasToMetaAnnotation.class, ConventionAliasMetaAnnotationTarget.class); assertThat(annotation.getString("value")).isEmpty(); - assertThat(annotation.getString("convention")).isEqualTo("convention"); + // Convention-based annotation attribute overrides are no longer supported as of + // Spring Framework 7.0. Otherwise, we would expect "convention". + assertThat(annotation.getString("convention")).isEmpty(); } @Test @@ -195,8 +204,7 @@ private static class WithExplicitAliasToMetaAnnotation { String value() default ""; - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = ConventionAliasMetaAnnotationTarget.class) + // Do NOT use @AliasFor here String convention() default ""; } diff --git a/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java deleted file mode 100644 index a1220d099cc4..000000000000 --- a/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferDecoderTests.java +++ /dev/null @@ -1,97 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.core.codec; - -import java.nio.charset.StandardCharsets; -import java.util.function.Consumer; - -import io.netty5.buffer.Buffer; -import io.netty5.buffer.DefaultBufferAllocators; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.core.ResolvableType; -import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.core.testfixture.codec.AbstractDecoderTests; -import org.springframework.util.MimeTypeUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Arjen Poutsma - */ -class Netty5BufferDecoderTests extends AbstractDecoderTests { - - private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); - - private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); - - - Netty5BufferDecoderTests() { - super(new Netty5BufferDecoder()); - } - - @Override - @Test - protected void canDecode() { - assertThat(this.decoder.canDecode(ResolvableType.forClass(Buffer.class), - MimeTypeUtils.TEXT_PLAIN)).isTrue(); - assertThat(this.decoder.canDecode(ResolvableType.forClass(Integer.class), - MimeTypeUtils.TEXT_PLAIN)).isFalse(); - assertThat(this.decoder.canDecode(ResolvableType.forClass(Buffer.class), - MimeTypeUtils.APPLICATION_JSON)).isTrue(); - } - - @Override - @Test - protected void decode() { - Flux input = Flux.concat( - dataBuffer(this.fooBytes), - dataBuffer(this.barBytes)); - - testDecodeAll(input, Buffer.class, step -> step - .consumeNextWith(expectByteBuffer(DefaultBufferAllocators.preferredAllocator().copyOf(this.fooBytes))) - .consumeNextWith(expectByteBuffer(DefaultBufferAllocators.preferredAllocator().copyOf(this.barBytes))) - .verifyComplete()); - } - - @Override - @Test - protected void decodeToMono() { - Flux input = Flux.concat( - dataBuffer(this.fooBytes), - dataBuffer(this.barBytes)); - - Buffer expected = DefaultBufferAllocators.preferredAllocator().allocate(this.fooBytes.length + this.barBytes.length) - .writeBytes(this.fooBytes) - .writeBytes(this.barBytes) - .readerOffset(0); - - testDecodeToMonoAll(input, Buffer.class, step -> step - .consumeNextWith(expectByteBuffer(expected)) - .verifyComplete()); - } - - private Consumer expectByteBuffer(Buffer expected) { - return actual -> { - try (actual; expected) { - assertThat(actual).isEqualTo(expected); - } - }; - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferEncoderTests.java deleted file mode 100644 index 2312971622a4..000000000000 --- a/spring-core/src/test/java/org/springframework/core/codec/Netty5BufferEncoderTests.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.codec; - -import java.nio.charset.StandardCharsets; - -import io.netty5.buffer.Buffer; -import io.netty5.buffer.DefaultBufferAllocators; -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Flux; - -import org.springframework.core.ResolvableType; -import org.springframework.core.testfixture.codec.AbstractEncoderTests; -import org.springframework.util.MimeTypeUtils; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * @author Arjen Poutsma - */ -class Netty5BufferEncoderTests extends AbstractEncoderTests { - - private final byte[] fooBytes = "foo".getBytes(StandardCharsets.UTF_8); - - private final byte[] barBytes = "bar".getBytes(StandardCharsets.UTF_8); - - Netty5BufferEncoderTests() { - super(new Netty5BufferEncoder()); - } - - @Test - @Override - public void canEncode() { - assertThat(this.encoder.canEncode(ResolvableType.forClass(Buffer.class), - MimeTypeUtils.TEXT_PLAIN)).isTrue(); - assertThat(this.encoder.canEncode(ResolvableType.forClass(Integer.class), - MimeTypeUtils.TEXT_PLAIN)).isFalse(); - assertThat(this.encoder.canEncode(ResolvableType.forClass(Buffer.class), - MimeTypeUtils.APPLICATION_JSON)).isTrue(); - - // gh-20024 - assertThat(this.encoder.canEncode(ResolvableType.NONE, null)).isFalse(); - } - - @Test - @Override - @SuppressWarnings("resource") - public void encode() { - Flux input = Flux.just(this.fooBytes, this.barBytes) - .map(DefaultBufferAllocators.preferredAllocator()::copyOf); - - testEncodeAll(input, Buffer.class, step -> step - .consumeNextWith(expectBytes(this.fooBytes)) - .consumeNextWith(expectBytes(this.barBytes)) - .verifyComplete()); - } - -} diff --git a/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java b/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java index 0d325a2eef39..1e45d29949c7 100644 --- a/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java +++ b/spring-core/src/test/java/org/springframework/core/codec/ResourceEncoderTests.java @@ -18,6 +18,7 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -29,7 +30,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.testfixture.codec.AbstractEncoderTests; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; 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 65d519f54087..f72f2069cd31 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 @@ void andThenWhenGivenConverterThenComposesInOrder() { } @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 81d61747949a..5e07edd8f3bf 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. @@ -43,18 +43,21 @@ 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; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; 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 @@ void convertCannotOptimizeArray() { 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 @@ public Long getId() { 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/convert/support/GenericConversionServiceTests.java b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java index 2c3a24a6c36e..77a72350808e 100644 --- a/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java +++ b/spring-core/src/test/java/org/springframework/core/convert/support/GenericConversionServiceTests.java @@ -30,6 +30,7 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.convert.ConversionFailedException; @@ -41,7 +42,6 @@ import org.springframework.core.convert.converter.GenericConverter; import org.springframework.core.io.DescriptiveResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import static java.util.Comparator.naturalOrder; @@ -710,8 +710,7 @@ public Set getConvertibleTypes() { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return null; } } @@ -733,8 +732,7 @@ public boolean matches(TypeDescriptor sourceType, TypeDescriptor targetType) { } @Override - @Nullable - public Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return null; } 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 000000000000..81d8e0ad6e8b --- /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/CustomEnvironmentTests.java b/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java index c408bce244a6..cbd026ebbe71 100644 --- a/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java +++ b/spring-core/src/test/java/org/springframework/core/env/CustomEnvironmentTests.java @@ -20,10 +20,9 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -109,13 +108,11 @@ protected Set getReservedDefaultProfiles() { void withNoProfileProperties() { ConfigurableEnvironment env = new AbstractEnvironment() { @Override - @Nullable - protected String doGetActiveProfilesProperty() { + protected @Nullable String doGetActiveProfilesProperty() { return null; } @Override - @Nullable - protected String doGetDefaultProfilesProperty() { + protected @Nullable String doGetDefaultProfilesProperty() { return null; } }; @@ -143,8 +140,7 @@ public CustomPropertySourcesPropertyResolver(PropertySources propertySources) { super(propertySources); } @Override - @Nullable - public String getProperty(String key) { + public @Nullable String getProperty(String key) { return super.getProperty(key) + "-test"; } } 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 23654dfe10e5..514052e47c6b 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.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 @@ */ 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 @@ void getProperty_withDefaultValue() { @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 @@ void getProperty_withNonConvertibleTargetType() { 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 @@ void getProperty_doesNotCache_replaceExistingKeyPostConstruction() { 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 @@ void getProperty_doesNotCache_replaceExistingKeyPostConstruction() { @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 @@ void getProperty_doesNotCache_addNewKeyPostConstruction() { @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 @@ void getPropertySources_replacePropertySource() { // 'system' now has precedence assertThat(propertyResolver.getProperty("foo")).isEqualTo("newValue"); - assertThat(propertySources).hasSize(2); + assertThat(propertySources).hasSize(3); } @Test @@ -170,81 +163,65 @@ void getRequiredProperty() { 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 @@ void setRequiredProperties_andValidateRequiredProperties() { 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 @@ void resolveNestedPropertyPlaceholders() { 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 @@ void ignoreUnresolvableNestedPlaceholdersIsConfigurable() { // 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 @@ void ignoreUnresolvableNestedPlaceholdersIsConfigurable() { // 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/PathResourceTests.java b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java index c096c9c88765..909e8a7f1e40 100644 --- a/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/PathResourceTests.java @@ -50,6 +50,7 @@ * @author Juergen Hoeller * @author Arjen Poutsma */ +@SuppressWarnings("removal") class PathResourceTests { private static final String TEST_DIR = 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 a425f389b0d0..d20bcf64df6a 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 @@ -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.core.io.buffer; +import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.io.OutputStream; import java.nio.ByteBuffer; @@ -28,7 +29,6 @@ import static org.assertj.core.api.Assertions.assertThatException; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.junit.jupiter.api.Assumptions.assumeFalse; /** * @author Arjen Poutsma @@ -342,6 +342,48 @@ void inputStream(DataBufferFactory bufferFactory) throws Exception { 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); } @@ -414,9 +456,6 @@ void increaseCapacity(DataBufferFactory bufferFactory) { @ParameterizedDataBufferAllocatingTest @SuppressWarnings("deprecation") void decreaseCapacityLowReadPosition(DataBufferFactory bufferFactory) { - assumeFalse(bufferFactory instanceof Netty5DataBufferFactory, - "Netty 5 does not support decreasing the capacity"); - super.bufferFactory = bufferFactory; DataBuffer buffer = createDataBuffer(2); @@ -430,9 +469,6 @@ void decreaseCapacityLowReadPosition(DataBufferFactory bufferFactory) { @ParameterizedDataBufferAllocatingTest @SuppressWarnings("deprecation") void decreaseCapacityHighReadPosition(DataBufferFactory bufferFactory) { - assumeFalse(bufferFactory instanceof Netty5DataBufferFactory, - "Netty 5 does not support decreasing the capacity"); - super.bufferFactory = bufferFactory; DataBuffer buffer = createDataBuffer(2); @@ -543,11 +579,6 @@ void asByteBufferIndexLength(DataBufferFactory bufferFactory) { ByteBuffer result = buffer.asByteBuffer(1, 2); assertThat(result.capacity()).isEqualTo(2); - assumeFalse(bufferFactory instanceof Netty5DataBufferFactory, () -> { - DataBufferUtils.release(buffer); - return "Netty 5 does share the internal buffer"; - }); - buffer.write((byte) 'c'); assertThat(result.remaining()).isEqualTo(2); @@ -561,9 +592,6 @@ void asByteBufferIndexLength(DataBufferFactory bufferFactory) { @ParameterizedDataBufferAllocatingTest @SuppressWarnings("deprecation") void byteBufferContainsDataBufferChanges(DataBufferFactory bufferFactory) { - assumeFalse(bufferFactory instanceof Netty5DataBufferFactory, - "Netty 5 does not support sharing data between buffers"); - super.bufferFactory = bufferFactory; DataBuffer dataBuffer = createDataBuffer(1); @@ -581,9 +609,6 @@ void byteBufferContainsDataBufferChanges(DataBufferFactory bufferFactory) { @ParameterizedDataBufferAllocatingTest @SuppressWarnings("deprecation") void dataBufferContainsByteBufferChanges(DataBufferFactory bufferFactory) { - assumeFalse(bufferFactory instanceof Netty5DataBufferFactory, - "Netty 5 does not support sharing data between buffers"); - super.bufferFactory = bufferFactory; DataBuffer dataBuffer = createDataBuffer(1); @@ -833,18 +858,13 @@ void slice(DataBufferFactory bufferFactory) { result = new byte[2]; slice.read(result); - if (!(bufferFactory instanceof Netty5DataBufferFactory)) { - assertThat(result).isEqualTo(new byte[]{'b', 'c'}); - } + assertThat(result).isEqualTo(new byte[]{'b', 'c'}); release(buffer); } @ParameterizedDataBufferAllocatingTest @SuppressWarnings("deprecation") void retainedSlice(DataBufferFactory bufferFactory) { - assumeFalse(bufferFactory instanceof Netty5DataBufferFactory, - "Netty 5 does not support retainedSlice"); - super.bufferFactory = bufferFactory; DataBuffer buffer = createDataBuffer(3); @@ -887,9 +907,7 @@ void spr16351(DataBufferFactory bufferFactory) { assertThat(result).isEqualTo(bytes); - if (bufferFactory instanceof Netty5DataBufferFactory) { - release(slice); - } + release(slice); 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 ee8b7352dad8..bde76e830cda 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.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; @@ -51,8 +53,10 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; +import org.springframework.core.io.UrlResource; import org.springframework.util.ClassUtils; import org.springframework.util.FileSystemUtils; +import org.springframework.util.ResourceUtils; import org.springframework.util.StreamUtils; import org.springframework.util.StringUtils; @@ -130,9 +134,11 @@ void encodedHashtagInPath() throws IOException { Path rootDir = Paths.get("src/test/resources/custom%23root").toAbsolutePath(); URL root = new URL("file:" + rootDir + "/"); resolver = new PathMatchingResourcePatternResolver(new DefaultResourceLoader(new URLClassLoader(new URL[] {root}))); + resolver.setUseCaches(false); assertExactFilenames("classpath*:scanned/*.txt", "resource#test1.txt", "resource#test2.txt"); } + @Nested class WithHashtagsInTheirFilenames { @@ -294,17 +300,28 @@ void classpathStarWithPatternInJar() { @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"); } } + @Nested class ClassPathManifestEntries { @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"); @@ -313,8 +330,8 @@ void javaDashJarFindsClassPathManifestEntries() throws Exception { writeApplicationJar(this.temp.resolve("app.jar")); String java = ProcessHandle.current().info().command().get(); Process process = new ProcessBuilder(java, "-jar", "app.jar") - .directory(this.temp.toFile()) - .start(); + .directory(this.temp.toFile()) + .start(); assertThat(process.waitFor()).isZero(); String result = StreamUtils.copyToString(process.getInputStream(), StandardCharsets.UTF_8); assertThat(result.replace("\\", "/")).contains("!!!!").contains("/lib/asset.jar!/assets/file.txt"); @@ -328,6 +345,22 @@ private void writeAssetJar(Path path) throws Exception { 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 { @@ -338,8 +371,7 @@ private void writeApplicationJar(Path path) throws Exception { mainAttributes.put(Name.MANIFEST_VERSION, "1.0"); try (JarOutputStream jar = new JarOutputStream(new FileOutputStream(path.toFile()), manifest)) { String appClassResource = ClassUtils.convertClassNameToResourcePath( - ClassPathManifestEntriesTestApplication.class.getName()) - + ClassUtils.CLASS_FILE_SUFFIX; + ClassPathManifestEntriesTestApplication.class.getName()) + ClassUtils.CLASS_FILE_SUFFIX; String folder = ""; for (String name : appClassResource.split("/")) { if (!name.endsWith(ClassUtils.CLASS_FILE_SUFFIX)) { @@ -356,18 +388,19 @@ private void writeApplicationJar(Path path) throws Exception { } } } + assertThat(new FileSystemResource(path).exists()).isTrue(); + assertThat(new UrlResource(ResourceUtils.JAR_URL_PREFIX + ResourceUtils.FILE_URL_PREFIX + path + ResourceUtils.JAR_URL_SEPARATOR).exists()).isTrue(); } private String buildSpringClassPath() throws Exception { - return copyClasses(PathMatchingResourcePatternResolver.class, "spring-core") - + copyClasses(LogFactory.class, "commons-logging"); + return copyClasses(PathMatchingResourcePatternResolver.class, "spring-core") + + copyClasses(LogFactory.class, "commons-logging"); } - private String copyClasses(Class sourceClass, String destinationName) - throws URISyntaxException, IOException { + private String copyClasses(Class sourceClass, String destinationName) throws URISyntaxException, IOException { Path destination = this.temp.resolve(destinationName); - String resourcePath = ClassUtils.convertClassNameToResourcePath(sourceClass.getName()) - + ClassUtils.CLASS_FILE_SUFFIX; + String resourcePath = ClassUtils.convertClassNameToResourcePath( + sourceClass.getName()) + ClassUtils.CLASS_FILE_SUFFIX; URL resource = getClass().getClassLoader().getResource(resourcePath); URL url = new URL(resource.toString().replace(resourcePath, "")); URLConnection connection = url.openConnection(); @@ -393,7 +426,6 @@ private String copyClasses(Class sourceClass, String destinationName) } return destinationName + "/ "; } - } diff --git a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java index 541da8c9f20a..06a452932138 100644 --- a/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java +++ b/spring-core/src/test/java/org/springframework/core/io/support/SpringFactoriesLoaderTests.java @@ -99,8 +99,8 @@ void loadWhenPackagePrivateFactory() { void loadWhenIncompatibleTypeThrowsException() { assertThatIllegalArgumentException() .isThrownBy(() -> SpringFactoriesLoader.forDefaultResourceLocation().load(String.class)) - .withMessageContaining("Unable to instantiate factory class " - + "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]"); + .withMessageContaining("Unable to instantiate factory class " + + "[org.springframework.core.io.support.MyDummyFactory1] for factory type [java.lang.String]"); } @Test @@ -127,8 +127,8 @@ void loadWhenMultipleConstructorsThrowsException() { assertThatIllegalArgumentException() .isThrownBy(() -> SpringFactoriesLoader.forDefaultResourceLocation(LimitedClassLoader.multipleArgumentFactories) .load(DummyFactory.class, resolver)) - .withMessageContaining("Unable to instantiate factory class " - + "[org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] for factory type [org.springframework.core.io.support.DummyFactory]") + .withMessageContaining("Unable to instantiate factory class " + + "[org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] for factory type [org.springframework.core.io.support.DummyFactory]") .havingRootCause().withMessageContaining("Class [org.springframework.core.io.support.MultipleConstructorArgsDummyFactory] has no suitable constructor"); } diff --git a/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java b/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java index 993c910a6c48..2747daf6d775 100644 --- a/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.java +++ b/spring-core/src/test/java/org/springframework/core/log/CompositeLogTests.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,11 +21,10 @@ import org.apache.commons.logging.Log; import org.junit.jupiter.api.Test; -import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; - +import static org.mockito.Mockito.when; /** * Tests for {@link CompositeLog}. 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 000000000000..1e44f37d7812 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java @@ -0,0 +1,134 @@ +/* + * Copyright 2002-2025 the original author 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.junit.jupiter.api.Test; + +import org.springframework.core.retry.support.MaxRetryAttemptsPolicy; +import org.springframework.util.backoff.FixedBackOff; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +/** + * Tests for {@link RetryTemplate}. + * + * @author Mahmoud Ben Hassine + * @author Sam Brannen + * @since 7.0 + */ +class RetryTemplateTests { + + private final RetryTemplate retryTemplate = new RetryTemplate(); + + @Test + void retryWithSuccess() throws Exception { + RetryCallback retryCallback = new RetryCallback<>() { + + int failure; + + @Override + public String run() throws Exception { + if (failure++ < 2) { + throw new Exception("Error while invoking greeting service"); + } + return "hello world"; + } + + @Override + public String getName() { + return "greeting service"; + } + }; + + retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE)); + + assertThat(retryTemplate.execute(retryCallback)).isEqualTo("hello world"); + } + + @Test + void retryWithFailure() { + Exception exception = new Exception("Error while invoking greeting service"); + + RetryCallback retryCallback = new RetryCallback<>() { + @Override + public String run() throws Exception { + throw exception; + } + + @Override + public String getName() { + return "greeting service"; + } + }; + + retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE)); + + assertThatExceptionOfType(RetryException.class) + .isThrownBy(() -> retryTemplate.execute(retryCallback)) + .withMessage("Retry policy for callback 'greeting service' exhausted; aborting execution") + .withCause(exception); + } + + @Test + void retrySpecificException() { + + @SuppressWarnings("serial") + class TechnicalException extends Exception { + public TechnicalException(String message) { + super(message); + } + } + + TechnicalException technicalException = new TechnicalException("Error while invoking greeting service"); + + RetryCallback retryCallback = new RetryCallback<>() { + @Override + public String run() throws TechnicalException { + throw technicalException; + } + + @Override + public String getName() { + return "greeting service"; + } + }; + + MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy() { + @Override + public RetryExecution start() { + return new RetryExecution() { + + int retryAttempts; + + @Override + public boolean shouldRetry(Throwable throwable) { + return this.retryAttempts++ < 3 && throwable instanceof TechnicalException; + } + }; + } + }; + retryTemplate.setRetryPolicy(retryPolicy); + retryTemplate.setBackOffPolicy(new FixedBackOff(100, Long.MAX_VALUE)); + + assertThatExceptionOfType(RetryException.class) + .isThrownBy(() -> retryTemplate.execute(retryCallback)) + .withMessage("Retry policy for callback 'greeting service' exhausted; aborting execution") + .withCause(technicalException); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/ComposedRetryListenerTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/ComposedRetryListenerTests.java new file mode 100644 index 000000000000..7ef1247c9935 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/support/ComposedRetryListenerTests.java @@ -0,0 +1,80 @@ +/* + * Copyright 2002-2025 the original author 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.Arrays; + +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 + */ +class ComposedRetryListenerTests { + + private final RetryListener listener1 = mock(); + private final RetryListener listener2 = mock(); + + private final CompositeRetryListener composedRetryListener = new CompositeRetryListener(Arrays.asList(listener1, listener2)); + + @Test + void beforeRetry() { + RetryExecution retryExecution = mock(); + this.composedRetryListener.beforeRetry(retryExecution); + + verify(this.listener1).beforeRetry(retryExecution); + verify(this.listener2).beforeRetry(retryExecution); + } + + @Test + void onSuccess() { + Object result = new Object(); + RetryExecution retryExecution = mock(); + this.composedRetryListener.onRetrySuccess(retryExecution, result); + + verify(this.listener1).onRetrySuccess(retryExecution, result); + verify(this.listener2).onRetrySuccess(retryExecution, result); + } + + @Test + void onFailure() { + Exception exception = new Exception(); + RetryExecution retryExecution = mock(); + this.composedRetryListener.onRetryFailure(retryExecution, exception); + + verify(this.listener1).onRetryFailure(retryExecution, exception); + verify(this.listener2).onRetryFailure(retryExecution, exception); + } + + @Test + void onMaxAttempts() { + Exception exception = new Exception(); + RetryExecution retryExecution = mock(); + this.composedRetryListener.onRetryPolicyExhaustion(retryExecution, exception); + + verify(this.listener1).onRetryPolicyExhaustion(retryExecution, exception); + verify(this.listener2).onRetryPolicyExhaustion(retryExecution, exception); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java new file mode 100644 index 000000000000..2912f5b9a124 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java @@ -0,0 +1,57 @@ +/* + * Copyright 2002-2025 the original author 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 org.junit.jupiter.api.Test; + +import org.springframework.core.retry.RetryExecution; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link MaxRetryAttemptsPolicy}. + * + * @author Mahmoud Ben Hassine + */ +class MaxRetryAttemptsPolicyTests { + + @Test + void defaultMaxRetryAttempts() { + // given + MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy(); + Throwable throwable = mock(); + + // when + RetryExecution retryExecution = retryPolicy.start(); + + // then + assertThat(retryExecution.shouldRetry(throwable)).isTrue(); + assertThat(retryExecution.shouldRetry(throwable)).isTrue(); + assertThat(retryExecution.shouldRetry(throwable)).isTrue(); + assertThat(retryExecution.shouldRetry(throwable)).isFalse(); + } + + @Test + void invalidMaxRetryAttempts() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new MaxRetryAttemptsPolicy(-1)) + .withMessage("Max retry attempts must be greater than zero"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java new file mode 100644 index 000000000000..3f8a82896e5d --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java @@ -0,0 +1,39 @@ +/* + * Copyright 2002-2025 the original author 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.time.Duration; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; + +/** + * Tests for {@link MaxRetryDurationPolicy}. + * + * @author Mahmoud Ben Hassine + */ +class MaxRetryDurationPolicyTests { + + @Test + void invalidMaxRetryDuration() { + assertThatIllegalArgumentException() + .isThrownBy(() -> new MaxRetryDurationPolicy(Duration.ZERO)) + .withMessage("Max retry duration must be positive"); + } + +} diff --git a/spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java b/spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java new file mode 100644 index 000000000000..2ddac2f85886 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java @@ -0,0 +1,52 @@ +/* + * Copyright 2002-2025 the original author 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.function.Predicate; + +import org.junit.jupiter.api.Test; + +import org.springframework.core.retry.RetryExecution; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link PredicateRetryPolicy}. + * + * @author Mahmoud Ben Hassine + */ +class PredicateRetryPolicyTests { + + @Test + void predicateRetryPolicy() { + // given + class MyException extends Exception { + @java.io.Serial + private static final long serialVersionUID = 1L; + } + Predicate predicate = MyException.class::isInstance; + PredicateRetryPolicy retryPolicy = new PredicateRetryPolicy(predicate); + + // when + RetryExecution retryExecution = retryPolicy.start(); + + // then + assertThat(retryExecution.shouldRetry(new MyException())).isTrue(); + assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse(); + } + +} 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 f26f9b5b7b86..02d671ee81bb 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.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 c7f4bd9d3b47..27ea6a7053f4 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.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 @@ */ 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 @@ void cannotExecuteWhenConcurrencyIsSwitchedOff() { } @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 @@ private static final class NoOpRunnable implements Runnable { @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 cc966f179955..35ad18fdf7e1 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 @@ -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,14 +16,17 @@ package org.springframework.core.type; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; +import java.util.Arrays; +import java.util.List; +import kotlin.Metadata; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.MergedAnnotation; -import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerClass; -import org.springframework.core.type.AbstractAnnotationMetadataTests.TestMemberClass.TestMemberClassInnerInterface; import org.springframework.util.MultiValueMap; import static org.assertj.core.api.Assertions.assertThat; @@ -34,376 +37,537 @@ * * @author Phillip Webb * @author Sam Brannen + * @author Brian Clozel */ public abstract class AbstractAnnotationMetadataTests { - @Test - void verifyEquals() { - AnnotationMetadata testClass1 = get(TestClass.class); - AnnotationMetadata testClass2 = get(TestClass.class); - AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); - AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); + protected abstract AnnotationMetadata get(Class source); - assertThat(testClass1).isNotEqualTo(null); + @Nested + class TypeTests { - assertThat(testClass1).isEqualTo(testClass1); - assertThat(testClass2).isEqualTo(testClass2); - assertThat(testClass1).isEqualTo(testClass2); - assertThat(testClass2).isEqualTo(testClass1); + @Test + void classEquals() { + AnnotationMetadata testClass1 = get(TestClass.class); + AnnotationMetadata testClass2 = get(TestClass.class); - assertThat(testMemberClass1).isEqualTo(testMemberClass1); - assertThat(testMemberClass2).isEqualTo(testMemberClass2); - assertThat(testMemberClass1).isEqualTo(testMemberClass2); - assertThat(testMemberClass2).isEqualTo(testMemberClass1); + assertThat(testClass1).isEqualTo(testClass1); + assertThat(testClass2).isEqualTo(testClass2); + assertThat(testClass1).isEqualTo(testClass2); + assertThat(testClass2).isEqualTo(testClass1); + } - assertThat(testClass1).isNotEqualTo(testMemberClass1); - assertThat(testMemberClass1).isNotEqualTo(testClass1); - } + @Test + void memberClassEquals() { + AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); + AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); - @Test - void verifyHashCode() { - AnnotationMetadata testClass1 = get(TestClass.class); - AnnotationMetadata testClass2 = get(TestClass.class); - AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); - AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); + assertThat(testMemberClass1).isEqualTo(testMemberClass1); + assertThat(testMemberClass2).isEqualTo(testMemberClass2); + assertThat(testMemberClass1).isEqualTo(testMemberClass2); + assertThat(testMemberClass2).isEqualTo(testMemberClass1); + } - assertThat(testClass1).hasSameHashCodeAs(testClass2); - assertThat(testMemberClass1).hasSameHashCodeAs(testMemberClass2); + @Test + void classHashCode() { + AnnotationMetadata testClass1 = get(TestClass.class); + AnnotationMetadata testClass2 = get(TestClass.class); - assertThat(testClass1).doesNotHaveSameHashCodeAs(testMemberClass1); - } + assertThat(testClass1).hasSameHashCodeAs(testClass2); + } - @Test - void verifyToString() { - assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName()); - } + @Test + void memberClassHashCode() { + AnnotationMetadata testMemberClass1 = get(TestMemberClass.class); + AnnotationMetadata testMemberClass2 = get(TestMemberClass.class); - @Test - void getClassNameReturnsClassName() { - assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName()); - } + assertThat(testMemberClass1).hasSameHashCodeAs(testMemberClass2); + } - @Test - void isInterfaceWhenInterfaceReturnsTrue() { - assertThat(get(TestInterface.class).isInterface()).isTrue(); - assertThat(get(TestAnnotation.class).isInterface()).isTrue(); - } + @Test + void classToString() { + assertThat(get(TestClass.class).toString()).isEqualTo(TestClass.class.getName()); + } - @Test - void isInterfaceWhenNotInterfaceReturnsFalse() { - assertThat(get(TestClass.class).isInterface()).isFalse(); - } + @Test + void getClassNameReturnsClassName() { + assertThat(get(TestClass.class).getClassName()).isEqualTo(TestClass.class.getName()); + } - @Test - void isAnnotationWhenAnnotationReturnsTrue() { - assertThat(get(TestAnnotation.class).isAnnotation()).isTrue(); - } + @Test + void isInterfaceWhenInterfaceReturnsTrue() { + assertThat(get(TestInterface.class).isInterface()).isTrue(); + assertThat(get(TestAnnotation.class).isInterface()).isTrue(); + } - @Test - void isAnnotationWhenNotAnnotationReturnsFalse() { - assertThat(get(TestClass.class).isAnnotation()).isFalse(); - assertThat(get(TestInterface.class).isAnnotation()).isFalse(); - } + @Test + void isInterfaceWhenNotInterfaceReturnsFalse() { + assertThat(get(TestClass.class).isInterface()).isFalse(); + } - @Test - void isFinalWhenFinalReturnsTrue() { - assertThat(get(TestFinalClass.class).isFinal()).isTrue(); - } + @Test + void isAnnotationWhenAnnotationReturnsTrue() { + assertThat(get(TestAnnotation.class).isAnnotation()).isTrue(); + } - @Test - void isFinalWhenNonFinalReturnsFalse() { - assertThat(get(TestClass.class).isFinal()).isFalse(); - } + @Test + void isAnnotationWhenNotAnnotationReturnsFalse() { + assertThat(get(TestClass.class).isAnnotation()).isFalse(); + assertThat(get(TestInterface.class).isAnnotation()).isFalse(); + } - @Test - void isIndependentWhenIndependentReturnsTrue() { - assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue(); - assertThat(get(TestClass.class).isIndependent()).isTrue(); - } + @Test + void isFinalWhenFinalReturnsTrue() { + assertThat(get(TestFinalClass.class).isFinal()).isTrue(); + } - @Test - void isIndependentWhenNotIndependentReturnsFalse() { - assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse(); - } + @Test + void isFinalWhenNonFinalReturnsFalse() { + assertThat(get(TestClass.class).isFinal()).isFalse(); + } - @Test - void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { - assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo( - AbstractAnnotationMetadataTests.class.getName()); - } + @Test + void isIndependentWhenIndependentReturnsTrue() { + assertThat(get(AbstractAnnotationMetadataTests.class).isIndependent()).isTrue(); + assertThat(get(TestClass.class).isIndependent()).isTrue(); + } - @Test - void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { - assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull(); - } + @Test + void isIndependentWhenNotIndependentReturnsFalse() { + assertThat(get(TestNonStaticInnerClass.class).isIndependent()).isFalse(); + } - @Test - void getSuperClassNameWhenHasSuperClassReturnsName() { - assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName()); - assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName()); - } + @Test + void getEnclosingClassNameWhenHasEnclosingClassReturnsEnclosingClass() { + assertThat(get(TestClass.class).getEnclosingClassName()).isEqualTo( + AbstractAnnotationMetadataTests.TypeTests.class.getName()); + } - @Test - void getSuperClassNameWhenHasNoSuperClassReturnsNull() { - assertThat(get(Object.class).getSuperClassName()).isNull(); - assertThat(get(TestInterface.class).getSuperClassName()).isNull(); - assertThat(get(TestSubInterface.class).getSuperClassName()).isNull(); - } + @Test + void getEnclosingClassNameWhenHasNoEnclosingClassReturnsNull() { + assertThat(get(AbstractAnnotationMetadataTests.class).getEnclosingClassName()).isNull(); + } - @Test - void getInterfaceNamesWhenHasInterfacesReturnsNames() { - assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); - assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactlyInAnyOrder(TestInterface.class.getName()); - } + @Test + void getSuperClassNameWhenHasSuperClassReturnsName() { + assertThat(get(TestSubclass.class).getSuperClassName()).isEqualTo(TestClass.class.getName()); + assertThat(get(TestClass.class).getSuperClassName()).isEqualTo(Object.class.getName()); + } - @Test - void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { - assertThat(get(TestClass.class).getInterfaceNames()).isEmpty(); - } + @Test + void getSuperClassNameWhenHasNoSuperClassReturnsNull() { + assertThat(get(Object.class).getSuperClassName()).isNull(); + assertThat(get(TestInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); + assertThat(get(TestSubInterface.class).getSuperClassName()).isIn(null, "java.lang.Object"); + } - @Test - void getMemberClassNamesWhenHasMemberClassesReturnsNames() { - assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder( - TestMemberClassInnerClass.class.getName(), TestMemberClassInnerInterface.class.getName()); - } + @Test + void getSuperClassNameWhenPackageInfoReturnsNull() throws Exception { + Class packageClass = Class.forName(getClass().getPackageName() + ".package-info"); + assertThat(get(packageClass).getSuperClassName()).isNull(); + } - @Test - void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { - assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); - } + @Test + void getInterfaceNamesWhenHasInterfacesReturnsNames() { + assertThat(get(TestSubclass.class).getInterfaceNames()).containsExactly(TestInterface.class.getName()); + assertThat(get(TestSubInterface.class).getInterfaceNames()).containsExactly(TestInterface.class.getName()); + } - @Test - void getAnnotationsReturnsDirectAnnotations() { - assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) - .filteredOn(MergedAnnotation::isDirectlyPresent) - .extracting(a -> a.getType().getName()) - .containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); - } + @Test + void getInterfaceNamesWhenHasNoInterfacesReturnsEmptyArray() { + assertThat(get(TestClass.class).getInterfaceNames()).isEmpty(); + } - @Test - void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { - assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue(); - } + @Test + void getMemberClassNamesWhenHasMemberClassesReturnsNames() { + assertThat(get(TestMemberClass.class).getMemberClassNames()).containsExactlyInAnyOrder( + TestMemberClass.TestMemberClassInnerClass.class.getName(), TestMemberClass.TestMemberClassInnerInterface.class.getName()); + } - @Test - void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { - assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue(); - } + @Test + void getMemberClassNamesWhenHasNestedMemberClassesReturnsOnlyFirstLevel() { + assertThat(get(TestNestedMemberClass.class).getMemberClassNames()).containsOnly( + TestNestedMemberClass.TestMemberClassInnerClassA.class.getName(), + TestNestedMemberClass.TestMemberClassInnerClassB.class.getName()); + } - @Test - void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { - assertThat(get(TestClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse(); - } + @Test + void getMemberClassNamesWhenHasNoMemberClassesReturnsEmptyArray() { + assertThat(get(TestClass.class).getMemberClassNames()).isEmpty(); + } - @Test - void getAnnotationAttributesReturnsAttributes() { - assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName())) - .containsOnly(entry("name", "test"), entry("size", 1)); - } + public static class TestClass { + } - @Test - void getAllAnnotationAttributesReturnsAllAttributes() { - MultiValueMap attributes = - get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); - assertThat(attributes).containsOnlyKeys("name", "size"); - assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); - assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); - } + public interface TestInterface { + } - @Test - void getAnnotationTypesReturnsDirectAnnotations() { - AnnotationMetadata metadata = get(WithDirectAnnotations.class); - assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( - DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); - } + public interface TestSubInterface extends TestInterface { + } - @Test - void getMetaAnnotationTypesReturnsMetaAnnotations() { - AnnotationMetadata metadata = get(WithMetaAnnotations.class); - assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) - .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); - } + public @interface TestAnnotation { + } - @Test - void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { - assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue(); - } + public static final class TestFinalClass { + } - @Test - void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { - assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse(); - assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse(); - } + public class TestNonStaticInnerClass { + } - @Test - void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { - assertThat(get(TestClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse(); - } + public static class TestSubclass extends TestClass implements TestInterface { + } - @Test - void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { - assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse(); - } + public static class TestMemberClass { - @Test - void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { - assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue(); - assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue(); - } + public static class TestMemberClassInnerClass { + } - @Test - void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { - assertThat(get(TestClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse(); - } + interface TestMemberClassInnerInterface { + } - @Test - void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { - assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue(); - } + } - @Test - void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { - assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue(); - } + public static class TestNestedMemberClass { - @Test - void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { - assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse(); - assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse(); - } + public static class TestMemberClassInnerClassA { - @Test - void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { - assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName())) - .extracting(MethodMetadata::getMethodName) - .containsExactlyInAnyOrder("direct", "meta"); - } + public static class TestMemberClassInnerClassAA { - protected abstract AnnotationMetadata get(Class source); + } + } - @Retention(RetentionPolicy.RUNTIME) - public @interface DirectAnnotation1 { - } + public static class TestMemberClassInnerClassB { - @Retention(RetentionPolicy.RUNTIME) - public @interface DirectAnnotation2 { - } + } - @Retention(RetentionPolicy.RUNTIME) - @MetaAnnotation1 - public @interface MetaAnnotationRoot { - } + } - @Retention(RetentionPolicy.RUNTIME) - @MetaAnnotation2 - public @interface MetaAnnotation1 { } - @Retention(RetentionPolicy.RUNTIME) - public @interface MetaAnnotation2 { - } + @Nested + class AnnotationTests { - public static class TestClass { - } + @Test + void getAnnotationsReturnsDirectAnnotations() { + assertThat(get(WithDirectAnnotations.class).getAnnotations().stream()) + .filteredOn(MergedAnnotation::isDirectlyPresent) + .extracting(a -> a.getType().getName()) + .containsExactlyInAnyOrder(DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); + } - public interface TestInterface { - } + @Test + void isAnnotatedWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithDirectAnnotations.class).isAnnotated(DirectAnnotation1.class.getName())).isTrue(); + } - public interface TestSubInterface extends TestInterface { - } + @Test + void isAnnotatedWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotations.class).isAnnotated(MetaAnnotation2.class.getName())).isTrue(); + } - public @interface TestAnnotation { - } + @Test + void isAnnotatedWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(NoAnnotationClass.class).isAnnotated(DirectAnnotation1.class.getName())).isFalse(); + } - public static final class TestFinalClass { - } + @Test + void getAnnotationAttributesReturnsAttributes() { + assertThat(get(WithAnnotationAttributes.class).getAnnotationAttributes(AnnotationAttributes.class.getName())) + .containsOnly(entry("name", "test"), entry("size", 1)); + } - public class TestNonStaticInnerClass { - } + @Test + void getAllAnnotationAttributesReturnsAllAttributes() { + MultiValueMap attributes = + get(WithMetaAnnotationAttributes.class).getAllAnnotationAttributes(AnnotationAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("name", "size"); + assertThat(attributes.get("name")).containsExactlyInAnyOrder("m1", "m2"); + assertThat(attributes.get("size")).containsExactlyInAnyOrder(1, 2); + } - public static class TestSubclass extends TestClass implements TestInterface { - } + @Test + void getComplexAttributeTypesReturnsAll() { + MultiValueMap attributes = + get(WithComplexAttributeTypes.class).getAllAnnotationAttributes(ComplexAttributes.class.getName()); + assertThat(attributes).containsOnlyKeys("names", "count", "type", "subAnnotation"); + assertThat(attributes.get("names")).hasSize(1); + assertThat(attributes.get("names").get(0)).isEqualTo(new String[]{"first", "second"}); + assertThat(attributes.get("count")).containsExactlyInAnyOrder(TestEnum.ONE); + assertThat(attributes.get("type")).containsExactlyInAnyOrder(TestEnum.class); + assertThat(attributes.get("subAnnotation")).hasSize(1); + } - @DirectAnnotation1 - @DirectAnnotation2 - public static class WithDirectAnnotations { - } + @Test + void getComplexAttributeTypesReturnsAllWithKotlinMetadata() { + MultiValueMap attributes = + get(WithComplexAttributeTypes.class).getAllAnnotationAttributes(Metadata.class.getName()); + assertThat(attributes).containsKeys("k", "mv"); + int[] values = {42}; + assertThat(attributes.get("mv")).hasSize(1); + assertThat(attributes.get("mv").get(0)).isEqualTo(values); + } - @MetaAnnotationRoot - public static class WithMetaAnnotations { - } + @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); + } - public static class TestMemberClass { + @Test + void getRepeatableReturnsAttributes() { + MultiValueMap attributes = + get(WithRepeatableAnnotations.class).getAllAnnotationAttributes(RepeatableAnnotations.class.getName()); + assertThat(attributes).containsKeys("value"); + assertThat(attributes.get("value")).hasSize(1); + } - public static class TestMemberClassInnerClass { + @Test + void getAnnotationTypesReturnsDirectAnnotations() { + AnnotationMetadata metadata = get(WithDirectAnnotations.class); + assertThat(metadata.getAnnotationTypes()).containsExactlyInAnyOrder( + DirectAnnotation1.class.getName(), DirectAnnotation2.class.getName()); } - interface TestMemberClassInnerInterface { + @Test + void getMetaAnnotationTypesReturnsMetaAnnotations() { + AnnotationMetadata metadata = get(WithMetaAnnotations.class); + assertThat(metadata.getMetaAnnotationTypes(MetaAnnotationRoot.class.getName())) + .containsExactlyInAnyOrder(MetaAnnotation1.class.getName(), MetaAnnotation2.class.getName()); } - } + @Test + void hasAnnotationWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithDirectAnnotations.class).hasAnnotation(DirectAnnotation1.class.getName())).isTrue(); + } - public static class WithAnnotatedMethod { + @Test + void hasAnnotationWhenMatchesMetaAnnotationReturnsFalse() { + assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation1.class.getName())).isFalse(); + assertThat(get(WithMetaAnnotations.class).hasAnnotation(MetaAnnotation2.class.getName())).isFalse(); + } - @DirectAnnotation1 - public void test() { + @Test + void hasAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(NoAnnotationClass.class).hasAnnotation(DirectAnnotation1.class.getName())).isFalse(); } - } + @Test + void hasMetaAnnotationWhenMatchesDirectReturnsFalse() { + assertThat(get(WithDirectAnnotations.class).hasMetaAnnotation(DirectAnnotation1.class.getName())).isFalse(); + } - public static class WithMetaAnnotatedMethod { + @Test + void hasMetaAnnotationWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isTrue(); + assertThat(get(WithMetaAnnotations.class).hasMetaAnnotation(MetaAnnotation2.class.getName())).isTrue(); + } - @MetaAnnotationRoot - public void test() { + @Test + void hasMetaAnnotationWhenDoesNotMatchDirectOrMetaAnnotationReturnsFalse() { + assertThat(get(NoAnnotationClass.class).hasMetaAnnotation(MetaAnnotation1.class.getName())).isFalse(); } - } + @Test + void hasAnnotatedMethodsWhenMatchesDirectAnnotationReturnsTrue() { + assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isTrue(); + } + + @Test + void hasAnnotatedMethodsWhenMatchesMetaAnnotationReturnsTrue() { + assertThat(get(WithMetaAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isTrue(); + } - public static class WithNonAnnotatedMethod { + @Test + void hasAnnotatedMethodsWhenDoesNotMatchAnyAnnotationReturnsFalse() { + assertThat(get(WithAnnotatedMethod.class).hasAnnotatedMethods(MetaAnnotation2.class.getName())).isFalse(); + assertThat(get(WithNonAnnotatedMethod.class).hasAnnotatedMethods(DirectAnnotation1.class.getName())).isFalse(); + } - public void test() { + @Test + void getAnnotatedMethodsReturnsMatchingAnnotatedAndMetaAnnotatedMethods() { + assertThat(get(WithDirectAndMetaAnnotatedMethods.class).getAnnotatedMethods(MetaAnnotation2.class.getName())) + .extracting(MethodMetadata::getMethodName) + .containsExactlyInAnyOrder("direct", "meta"); } - } + public static class WithAnnotatedMethod { + + @DirectAnnotation1 + public void test() { + } + + } + + public static class WithMetaAnnotatedMethod { + + @MetaAnnotationRoot + public void test() { + } + + } + + public static class WithNonAnnotatedMethod { + + } + + public static class WithDirectAndMetaAnnotatedMethods { + + @MetaAnnotation2 + public void direct() { + } + + @MetaAnnotationRoot + public void meta() { + } + + } + + @AnnotationAttributes(name = "test", size = 1) + public static class WithAnnotationAttributes { + } + + @MetaAnnotationAttributes1 + @MetaAnnotationAttributes2 + public static class WithMetaAnnotationAttributes { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m1", size = 1) + public @interface MetaAnnotationAttributes1 { + } + + @Retention(RetentionPolicy.RUNTIME) + @AnnotationAttributes(name = "m2", size = 2) + public @interface MetaAnnotationAttributes2 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface AnnotationAttributes { + + String name(); + + int size(); + + } - public static class WithDirectAndMetaAnnotatedMethods { + @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 { + + String[] names(); + + TestEnum count(); + + Class type(); + + SubAnnotation subAnnotation(); + } + + public @interface SubAnnotation { + + String name(); + } + + public enum TestEnum { + ONE, TWO, THREE + } + + @RepeatableAnnotation(name = "first") + @RepeatableAnnotation(name = "second") + public static class WithRepeatableAnnotations { + + } + + @Retention(RetentionPolicy.RUNTIME) + @Repeatable(RepeatableAnnotations.class) + public @interface RepeatableAnnotation { + + String name(); + + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface RepeatableAnnotations { + + RepeatableAnnotation[] value(); + + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface DirectAnnotation2 { + } + + @Retention(RetentionPolicy.RUNTIME) + @MetaAnnotation1 + public @interface MetaAnnotationRoot { + } + + @Retention(RetentionPolicy.RUNTIME) @MetaAnnotation2 - public void direct() { + public @interface MetaAnnotation1 { + } + + @Retention(RetentionPolicy.RUNTIME) + public @interface MetaAnnotation2 { + } + + @DirectAnnotation1 + @DirectAnnotation2 + public static class WithDirectAnnotations { } @MetaAnnotationRoot - public void meta() { + public static class WithMetaAnnotations { } - } + static class NoAnnotationClass { - @AnnotationAttributes(name = "test", size = 1) - public static class WithAnnotationAttributes { - } + } - @MetaAnnotationAttributes1 - @MetaAnnotationAttributes2 - public static class WithMetaAnnotationAttributes { } - @Retention(RetentionPolicy.RUNTIME) - @AnnotationAttributes(name = "m1", size = 1) - public @interface MetaAnnotationAttributes1 { - } + @Nested + class MethodTests { - @Retention(RetentionPolicy.RUNTIME) - @AnnotationAttributes(name = "m2", size = 2) - public @interface MetaAnnotationAttributes2 { - } + @Test + void declaredMethodsToString() { + List methods = get(TestMethods.class).getDeclaredMethods().stream().map(Object::toString).toList(); + List expected = Arrays.stream(TestMethods.class.getDeclaredMethods()).map(Object::toString).toList(); + assertThat(methods).containsExactlyInAnyOrderElementsOf(expected); + } - @Retention(RetentionPolicy.RUNTIME) - public @interface AnnotationAttributes { + static class TestMethods { + public String test1(String argument) { + return "test"; + } - String name(); + public String test2(String argument) { + return "test"; + } - int size(); + public String test3(String argument) { + return "test"; + } + } } diff --git a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java index 6e829cddabd7..19266e7c2b73 100644 --- a/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/AnnotationMetadataTests.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. @@ -665,8 +665,7 @@ private static class AnnotatedComponentSubClass extends AnnotatedComponent { @Target(ElementType.TYPE) public @interface ComposedConfigurationWithAttributeOverrides { - // Do NOT use @AliasFor here until Spring 6.1 - // @AliasFor(annotation = TestComponentScan.class) + @AliasFor(annotation = TestComponentScan.class) String[] basePackages() default {}; } diff --git a/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java b/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java index 67647495ccce..00bcae566825 100644 --- a/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java +++ b/spring-core/src/test/java/org/springframework/core/type/CachingMetadataReaderLeakTests.java @@ -18,6 +18,7 @@ import java.net.URL; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.io.Resource; @@ -26,7 +27,6 @@ import org.springframework.core.type.classreading.CachingMetadataReaderFactory; import org.springframework.core.type.classreading.MetadataReader; import org.springframework.core.type.classreading.MetadataReaderFactory; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.core.testfixture.TestGroup.LONG_RUNNING; 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 000000000000..1a2b01ed9b78 --- /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 f66c81bc35d3..43e9fc848156 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 @@ -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. 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 e1352e5eeceb..6de3602cbfdc 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 @@ void patternComparator() { 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 @@ void patternComparator() { 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/CollectionUtilsTests.java b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java index 1a96590db745..f03ac62946a0 100644 --- a/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/CollectionUtilsTests.java @@ -31,10 +31,9 @@ import java.util.TreeSet; import java.util.Vector; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; - import static org.assertj.core.api.Assertions.assertThat; /** @@ -181,27 +180,29 @@ void findFirstMatch() { @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 diff --git a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java index 95a148ae9817..f39b95131c0b 100644 --- a/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java +++ b/spring-core/src/test/java/org/springframework/util/ConcurrentReferenceHashMapTests.java @@ -26,9 +26,9 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.util.ConcurrentReferenceHashMap.Entry; import org.springframework.util.ConcurrentReferenceHashMap.Reference; import org.springframework.util.ConcurrentReferenceHashMap.Restructure; 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 c121d1a3375c..c5bcb66574ef 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 @@ 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 @@ void deleteRecursively() throws Exception { } @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 @@ void copyRecursively() throws Exception { 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 @@ void copyRecursively() throws Exception { 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 b4618c090d78..d2ef171a30f5 100644 --- a/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java +++ b/spring-core/src/test/java/org/springframework/util/PatternMatchUtilsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -53,18 +53,22 @@ void trivial() { assertMatches(new String[] { null, "" }, ""); assertMatches(new String[] { null, "123" }, "123"); assertMatches(new String[] { null, "*" }, "123"); + + testMixedCaseMatch("abC", "Abc"); } @Test void startsWith() { assertMatches("get*", "getMe"); assertDoesNotMatch("get*", "setMe"); + testMixedCaseMatch("geT*", "GetMe"); } @Test void endsWith() { assertMatches("*Test", "getMeTest"); assertDoesNotMatch("*Test", "setMe"); + testMixedCaseMatch("*TeSt", "getMeTesT"); } @Test @@ -74,6 +78,10 @@ void between() { assertMatches("*stuff*", "stuffTest"); assertMatches("*stuff*", "getstuff"); assertMatches("*stuff*", "stuff"); + testMixedCaseMatch("*stuff*", "getStuffTest"); + testMixedCaseMatch("*stuff*", "StuffTest"); + testMixedCaseMatch("*stuff*", "getStuff"); + testMixedCaseMatch("*stuff*", "Stuff"); } @Test @@ -82,6 +90,8 @@ void startsEnds() { assertMatches("on*Event", "onEvent"); assertDoesNotMatch("3*3", "3"); assertMatches("3*3", "33"); + testMixedCaseMatch("on*Event", "OnMyEvenT"); + testMixedCaseMatch("on*Event", "OnEvenT"); } @Test @@ -122,18 +132,27 @@ void patternVariants() { private void assertMatches(String pattern, String str) { assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isTrue(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue(); } private void assertDoesNotMatch(String pattern, String str) { assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isFalse(); + } + + private void testMixedCaseMatch(String pattern, String str) { + assertThat(PatternMatchUtils.simpleMatch(pattern, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(pattern, str)).isTrue(); } private void assertMatches(String[] patterns, String str) { assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isTrue(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isTrue(); } private void assertDoesNotMatch(String[] patterns, String str) { assertThat(PatternMatchUtils.simpleMatch(patterns, str)).isFalse(); + assertThat(PatternMatchUtils.simpleMatchIgnoreCase(patterns, str)).isFalse(); } } diff --git a/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java b/spring-core/src/test/java/org/springframework/util/PlaceholderParserTests.java index 53182acf882e..b16f741517aa 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.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 OnlyPlaceholderTests { @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 @@ static Stream placeholders() { @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 @@ static Stream nestedPlaceholders() { @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 @@ void parseWithMultiplePlaceholdersAndText() { 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 @@ void placeholdersWithSeparatorAreHandledAsIs() { @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 DefaultValueTests { @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 @@ static Stream placeholders() { @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 @@ static Stream nestedPlaceholders() { @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 @@ static Stream exactMatchPlaceholders() { @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 @@ static Stream escapedPlaceholders() { 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 @@ static Stream escapedSeparators() { ); } + @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 ExceptionTests { @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 @@ PlaceholderResolver mockPlaceholderResolver(String... pairs) { 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 c5e321188e5e..297b5128dbac 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 @@ void getFilename() { 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/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java deleted file mode 100644 index 920b5ebd448d..000000000000 --- a/spring-core/src/test/java/org/springframework/util/concurrent/ListenableFutureTaskTests.java +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.io.IOException; -import java.util.concurrent.Callable; -import java.util.concurrent.ExecutionException; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoInteractions; - -/** - * @author Arjen Poutsma - * @author Sebastien Deleuze - */ -@SuppressWarnings({"deprecation", "removal"}) -class ListenableFutureTaskTests { - - @Test - void success() throws Exception { - final String s = "Hello World"; - Callable callable = () -> s; - - ListenableFutureTask task = new ListenableFutureTask<>(callable); - task.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - assertThat(result).isEqualTo(s); - } - - @Override - public void onFailure(Throwable ex) { - throw new AssertionError(ex.getMessage(), ex); - } - }); - task.run(); - - assertThat(task.get()).isSameAs(s); - assertThat(task.completable().get()).isSameAs(s); - task.completable().thenAccept(v -> assertThat(v).isSameAs(s)); - } - - @Test - void failure() { - final String s = "Hello World"; - Callable callable = () -> { - throw new IOException(s); - }; - - ListenableFutureTask task = new ListenableFutureTask<>(callable); - task.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - fail("onSuccess not expected"); - } - - @Override - public void onFailure(Throwable ex) { - assertThat(ex.getMessage()).isEqualTo(s); - } - }); - task.run(); - - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(task::get) - .havingCause() - .withMessage(s); - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(task.completable()::get) - .havingCause() - .withMessage(s); - } - - @Test - void successWithLambdas() throws Exception { - final String s = "Hello World"; - Callable callable = () -> s; - - SuccessCallback successCallback = mock(); - FailureCallback failureCallback = mock(); - ListenableFutureTask task = new ListenableFutureTask<>(callable); - task.addCallback(successCallback, failureCallback); - task.run(); - verify(successCallback).onSuccess(s); - verifyNoInteractions(failureCallback); - - assertThat(task.get()).isSameAs(s); - assertThat(task.completable().get()).isSameAs(s); - task.completable().thenAccept(v -> assertThat(v).isSameAs(s)); - } - - @Test - void failureWithLambdas() { - final String s = "Hello World"; - IOException ex = new IOException(s); - Callable callable = () -> { - throw ex; - }; - - SuccessCallback successCallback = mock(); - FailureCallback failureCallback = mock(); - ListenableFutureTask task = new ListenableFutureTask<>(callable); - task.addCallback(successCallback, failureCallback); - task.run(); - verify(failureCallback).onFailure(ex); - verifyNoInteractions(successCallback); - - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(task::get) - .havingCause() - .withMessage(s); - assertThatExceptionOfType(ExecutionException.class) - .isThrownBy(task.completable()::get) - .havingCause() - .withMessage(s); - } - -} diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java deleted file mode 100644 index 5b7a139f8407..000000000000 --- a/spring-core/src/test/java/org/springframework/util/concurrent/MonoToListenableFutureAdapterTests.java +++ /dev/null @@ -1,74 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.time.Duration; -import java.util.concurrent.Future; -import java.util.concurrent.atomic.AtomicReference; - -import org.junit.jupiter.api.Test; -import reactor.core.publisher.Mono; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link MonoToListenableFutureAdapter}. - * - * @author Rossen Stoyanchev - */ -@SuppressWarnings({"deprecation", "removal"}) -class MonoToListenableFutureAdapterTests { - - @Test - void success() { - String expected = "one"; - AtomicReference actual = new AtomicReference<>(); - ListenableFuture future = new MonoToListenableFutureAdapter<>(Mono.just(expected)); - future.addCallback(actual::set, actual::set); - - assertThat(actual.get()).isEqualTo(expected); - } - - @Test - @SuppressWarnings("deprecation") - void failure() { - Throwable expected = new IllegalStateException("oops"); - AtomicReference actual = new AtomicReference<>(); - ListenableFuture future = new MonoToListenableFutureAdapter<>(Mono.error(expected)); - future.addCallback(actual::set, actual::set); - - assertThat(actual.get()).isEqualTo(expected); - } - - @Test - void cancellation() { - Mono mono = Mono.delay(Duration.ofSeconds(60)); - Future future = new MonoToListenableFutureAdapter<>(mono); - - assertThat(future.cancel(true)).isTrue(); - assertThat(future.isCancelled()).isTrue(); - } - - @Test - void cancellationAfterTerminated() { - Future future = new MonoToListenableFutureAdapter<>(Mono.empty()); - - assertThat(future.cancel(true)).as("Should return false if task already completed").isFalse(); - assertThat(future.isCancelled()).isFalse(); - } - -} diff --git a/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java b/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java deleted file mode 100644 index d79296dd0990..000000000000 --- a/spring-core/src/test/java/org/springframework/util/concurrent/SettableListenableFutureTests.java +++ /dev/null @@ -1,414 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.util.concurrent; - -import java.util.concurrent.CancellationException; -import java.util.concurrent.ExecutionException; -import java.util.concurrent.Future; -import java.util.concurrent.TimeUnit; -import java.util.concurrent.TimeoutException; - -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; -import static org.assertj.core.api.Assertions.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; - -/** - * @author Mattias Severson - * @author Juergen Hoeller - */ -@SuppressWarnings({"deprecation", "removal"}) -class SettableListenableFutureTests { - - private final SettableListenableFuture settableListenableFuture = new SettableListenableFuture<>(); - - - @Test - void validateInitialValues() { - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isFalse(); - } - - @Test - void returnsSetValue() throws ExecutionException, InterruptedException { - String string = "hello"; - assertThat(settableListenableFuture.set(string)).isTrue(); - assertThat(settableListenableFuture.get()).isEqualTo(string); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void returnsSetValueFromCompletable() throws ExecutionException, InterruptedException { - String string = "hello"; - assertThat(settableListenableFuture.set(string)).isTrue(); - Future completable = settableListenableFuture.completable(); - assertThat(completable.get()).isEqualTo(string); - assertThat(completable.isCancelled()).isFalse(); - assertThat(completable.isDone()).isTrue(); - } - - @Test - void setValueUpdatesDoneStatus() { - settableListenableFuture.set("hello"); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void throwsSetExceptionWrappedInExecutionException() { - Throwable exception = new RuntimeException(); - assertThat(settableListenableFuture.setException(exception)).isTrue(); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy( - settableListenableFuture::get) - .withCause(exception); - - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void throwsSetExceptionWrappedInExecutionExceptionFromCompletable() { - Throwable exception = new RuntimeException(); - assertThat(settableListenableFuture.setException(exception)).isTrue(); - Future completable = settableListenableFuture.completable(); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy( - completable::get) - .withCause(exception); - - assertThat(completable.isCancelled()).isFalse(); - assertThat(completable.isDone()).isTrue(); - } - - @Test - void throwsSetErrorWrappedInExecutionException() { - Throwable exception = new OutOfMemoryError(); - assertThat(settableListenableFuture.setException(exception)).isTrue(); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy( - settableListenableFuture::get) - .withCause(exception); - - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void throwsSetErrorWrappedInExecutionExceptionFromCompletable() { - Throwable exception = new OutOfMemoryError(); - assertThat(settableListenableFuture.setException(exception)).isTrue(); - Future completable = settableListenableFuture.completable(); - - assertThatExceptionOfType(ExecutionException.class).isThrownBy( - completable::get) - .withCause(exception); - - assertThat(completable.isCancelled()).isFalse(); - assertThat(completable.isDone()).isTrue(); - } - - @Test - void setValueTriggersCallback() { - String string = "hello"; - final String[] callbackHolder = new String[1]; - - settableListenableFuture.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - callbackHolder[0] = result; - } - - @Override - public void onFailure(Throwable ex) { - throw new AssertionError("Expected onSuccess() to be called", ex); - } - }); - - settableListenableFuture.set(string); - assertThat(callbackHolder[0]).isEqualTo(string); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void setValueTriggersCallbackOnlyOnce() { - String string = "hello"; - final String[] callbackHolder = new String[1]; - - settableListenableFuture.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - callbackHolder[0] = result; - } - - @Override - public void onFailure(Throwable ex) { - throw new AssertionError("Expected onSuccess() to be called", ex); - } - }); - - settableListenableFuture.set(string); - assertThat(settableListenableFuture.set("good bye")).isFalse(); - assertThat(callbackHolder[0]).isEqualTo(string); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void setExceptionTriggersCallback() { - Throwable exception = new RuntimeException(); - final Throwable[] callbackHolder = new Throwable[1]; - - settableListenableFuture.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - fail("Expected onFailure() to be called"); - } - - @Override - public void onFailure(Throwable ex) { - callbackHolder[0] = ex; - } - }); - - settableListenableFuture.setException(exception); - assertThat(callbackHolder[0]).isEqualTo(exception); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void setExceptionTriggersCallbackOnlyOnce() { - Throwable exception = new RuntimeException(); - final Throwable[] callbackHolder = new Throwable[1]; - - settableListenableFuture.addCallback(new ListenableFutureCallback<>() { - @Override - public void onSuccess(String result) { - fail("Expected onFailure() to be called"); - } - - @Override - public void onFailure(Throwable ex) { - callbackHolder[0] = ex; - } - }); - - settableListenableFuture.setException(exception); - assertThat(settableListenableFuture.setException(new IllegalArgumentException())).isFalse(); - assertThat(callbackHolder[0]).isEqualTo(exception); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void nullIsAcceptedAsValueToSet() throws ExecutionException, InterruptedException { - settableListenableFuture.set(null); - assertThat(settableListenableFuture.get()).isNull(); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void getWaitsForCompletion() throws ExecutionException, InterruptedException { - final String string = "hello"; - - new Thread(() -> { - try { - Thread.sleep(20L); - settableListenableFuture.set(string); - } - catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - }).start(); - - String value = settableListenableFuture.get(); - assertThat(value).isEqualTo(string); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void getWithTimeoutThrowsTimeoutException() { - assertThatExceptionOfType(TimeoutException.class).isThrownBy(() -> - settableListenableFuture.get(1L, TimeUnit.MILLISECONDS)); - } - - @Test - void getWithTimeoutWaitsForCompletion() throws ExecutionException, InterruptedException, TimeoutException { - final String string = "hello"; - - new Thread(() -> { - try { - Thread.sleep(20L); - settableListenableFuture.set(string); - } - catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - }).start(); - - String value = settableListenableFuture.get(500L, TimeUnit.MILLISECONDS); - assertThat(value).isEqualTo(string); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void cancelPreventsValueFromBeingSet() { - assertThat(settableListenableFuture.cancel(true)).isTrue(); - assertThat(settableListenableFuture.set("hello")).isFalse(); - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void cancelSetsFutureToDone() { - settableListenableFuture.cancel(true); - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void cancelWithMayInterruptIfRunningTrueCallsOverriddenMethod() { - InterruptibleSettableListenableFuture interruptibleFuture = new InterruptibleSettableListenableFuture(); - assertThat(interruptibleFuture.cancel(true)).isTrue(); - assertThat(interruptibleFuture.calledInterruptTask()).isTrue(); - assertThat(interruptibleFuture.isCancelled()).isTrue(); - assertThat(interruptibleFuture.isDone()).isTrue(); - } - - @Test - void cancelWithMayInterruptIfRunningFalseDoesNotCallOverriddenMethod() { - InterruptibleSettableListenableFuture interruptibleFuture = new InterruptibleSettableListenableFuture(); - assertThat(interruptibleFuture.cancel(false)).isTrue(); - assertThat(interruptibleFuture.calledInterruptTask()).isFalse(); - assertThat(interruptibleFuture.isCancelled()).isTrue(); - assertThat(interruptibleFuture.isDone()).isTrue(); - } - - @Test - void setPreventsCancel() { - assertThat(settableListenableFuture.set("hello")).isTrue(); - assertThat(settableListenableFuture.cancel(true)).isFalse(); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void cancelPreventsExceptionFromBeingSet() { - assertThat(settableListenableFuture.cancel(true)).isTrue(); - assertThat(settableListenableFuture.setException(new RuntimeException())).isFalse(); - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void setExceptionPreventsCancel() { - assertThat(settableListenableFuture.setException(new RuntimeException())).isTrue(); - assertThat(settableListenableFuture.cancel(true)).isFalse(); - assertThat(settableListenableFuture.isCancelled()).isFalse(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void cancelStateThrowsExceptionWhenCallingGet() { - settableListenableFuture.cancel(true); - - assertThatExceptionOfType(CancellationException.class).isThrownBy(settableListenableFuture::get); - - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - void cancelStateThrowsExceptionWhenCallingGetWithTimeout() { - new Thread(() -> { - try { - Thread.sleep(20L); - settableListenableFuture.cancel(true); - } - catch (InterruptedException ex) { - throw new RuntimeException(ex); - } - }).start(); - - assertThatExceptionOfType(CancellationException.class).isThrownBy(() -> - settableListenableFuture.get(500L, TimeUnit.MILLISECONDS)); - - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - @SuppressWarnings({"rawtypes", "unchecked"}) - public void cancelDoesNotNotifyCallbacksOnSet() { - ListenableFutureCallback callback = mock(); - settableListenableFuture.addCallback(callback); - settableListenableFuture.cancel(true); - - verify(callback).onFailure(any(CancellationException.class)); - verifyNoMoreInteractions(callback); - - settableListenableFuture.set("hello"); - verifyNoMoreInteractions(callback); - - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - @Test - @SuppressWarnings({"rawtypes", "unchecked"}) - public void cancelDoesNotNotifyCallbacksOnSetException() { - ListenableFutureCallback callback = mock(); - settableListenableFuture.addCallback(callback); - settableListenableFuture.cancel(true); - - verify(callback).onFailure(any(CancellationException.class)); - verifyNoMoreInteractions(callback); - - settableListenableFuture.setException(new RuntimeException()); - verifyNoMoreInteractions(callback); - - assertThat(settableListenableFuture.isCancelled()).isTrue(); - assertThat(settableListenableFuture.isDone()).isTrue(); - } - - - private static class InterruptibleSettableListenableFuture extends SettableListenableFuture { - - private boolean interrupted = false; - - @Override - protected void interruptTask() { - interrupted = true; - } - - boolean calledInterruptTask() { - return interrupted; - } - } - -} diff --git a/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java index 7f51d8af088a..4e3c7e9d4de7 100644 --- a/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java +++ b/spring-core/src/test/java/org/springframework/util/xml/AbstractStaxXMLReaderTests.java @@ -29,6 +29,7 @@ import javax.xml.transform.dom.DOMResult; import javax.xml.transform.sax.SAXSource; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.invocation.InvocationOnMock; @@ -44,7 +45,6 @@ import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.tests.MockitoUtils; import org.springframework.tests.MockitoUtils.InvocationArgumentsAdapter; @@ -218,8 +218,8 @@ private static class CharArrayToStringAdapter implements InvocationArgumentsAdap @Override public Object[] adaptArguments(Object[] arguments) { - if (arguments.length == 3 && arguments[0] instanceof char[] - && arguments[1] instanceof Integer && arguments[2] instanceof Integer) { + if (arguments.length == 3 && arguments[0] instanceof char[] && + arguments[1] instanceof Integer && arguments[2] instanceof Integer) { return new Object[] {new String((char[]) arguments[0], (Integer) arguments[1], (Integer) arguments[2])}; } return arguments; @@ -271,10 +271,10 @@ public boolean equals(@Nullable Object obj) { for (int i = 0; i < other.getLength(); i++) { boolean found = false; for (int j = 0; j < attributes.getLength(); j++) { - if (other.getURI(i).equals(attributes.getURI(j)) - && other.getQName(i).equals(attributes.getQName(j)) - && other.getType(i).equals(attributes.getType(j)) - && other.getValue(i).equals(attributes.getValue(j))) { + if (other.getURI(i).equals(attributes.getURI(j)) && + other.getQName(i).equals(attributes.getQName(j)) && + other.getType(i).equals(attributes.getType(j)) && + other.getValue(i).equals(attributes.getValue(j))) { found = true; break; } 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 861f34325559..40d9b3361040 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 @@ -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,21 +65,20 @@ class BindingReflectionHintsRegistrarKotlinTests { @Test fun `Register reflection hints for Kotlin data class`() { bindingRegistrar.registerReflectionHints(hints.reflection(), SampleDataClass::class.java) - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "component1")).accepts(hints) - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "copy")).accepts(hints) - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "getName")).accepts(hints) - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "isNonNullable")).accepts(hints) - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleDataClass::class.java, "isNullable")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleDataClass::class.java, "component1")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleDataClass::class.java, "copy")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleDataClass::class.java, "getName")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleDataClass::class.java, "isNonNullable")).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleDataClass::class.java, "isNullable")).accepts(hints) val copyDefault: Method = SampleDataClass::class.java.getMethod("copy\$default", SampleDataClass::class.java, String::class.java, Boolean::class.javaPrimitiveType, Boolean::class.javaObjectType, Int::class.java, Object::class.java) - assertThat(RuntimeHintsPredicates.reflection().onMethod(copyDefault)).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(copyDefault)).accepts(hints) } @Test fun `Register reflection hints on declared methods for Kotlin class`() { bindingRegistrar.registerReflectionHints(hints.reflection(), SampleClass::class.java) - assertThat(RuntimeHintsPredicates.reflection().onType(SampleClass::class.java) - .withMemberCategory(MemberCategory.INTROSPECT_DECLARED_METHODS)).accepts(hints) + assertThat(RuntimeHintsPredicates.reflection().onType(SampleClass::class.java)).accepts(hints) } } 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 bb94dd3867fe..f1c8eabc1e96 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 31ebb74927d7..24a98d61ccb6 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/test/kotlin/org/springframework/core/NullnessKotlinTests.kt b/spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt new file mode 100644 index 000000000000..74db2eae47da --- /dev/null +++ b/spring-core/src/test/kotlin/org/springframework/core/NullnessKotlinTests.kt @@ -0,0 +1,81 @@ +/* + * Copyright 2002-2025 the original author 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 + +import org.assertj.core.api.Assertions +import org.junit.jupiter.api.Test +import kotlin.reflect.jvm.javaMethod + +/** + * Kotlin tests for [Nullness]. + * + * @author Sebastien Deleuze + */ +class NullnessKotlinTests { + + val nullableProperty: String? = "" + val nonNullProperty: String = "" + + @Test + fun nullableReturnType() { + val method = ::nullable.javaMethod!! + val nullness = Nullness.forMethodReturnType(method) + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE) + } + + @Test + fun nullableParameter() { + val method = ::nullable.javaMethod!! + val nullness = Nullness.forParameter(method.parameters[0]) + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE) + } + + @Test + fun nonNullReturnType() { + val method = ::nonNull.javaMethod!! + val nullness = Nullness.forMethodReturnType(method) + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL) + } + + @Test + fun nonNullParameter() { + val method = ::nonNull.javaMethod!! + val nullness = Nullness.forParameter(method.parameters[0]) + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL) + } + + @Test + fun nullableProperty() { + val field = javaClass.getDeclaredField("nullableProperty") + val nullness = Nullness.forField(field) + Assertions.assertThat(nullness).isEqualTo(Nullness.NULLABLE) + } + + @Test + fun nonNullProperty() { + val field = javaClass.getDeclaredField("nonNullProperty") + val nullness = Nullness.forField(field) + Assertions.assertThat(nullness).isEqualTo(Nullness.NON_NULL) + } + + @Suppress("unused_parameter") + fun nullable(nullable: String?): String? = "foo" + + @Suppress("unused_parameter") + fun nonNull(nonNull: String): String = "foo" + +} \ No newline at end of file diff --git a/spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json b/spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json new file mode 100644 index 000000000000..bb434fb22e2d --- /dev/null +++ b/spring-core/src/test/resources/org/springframework/aot/nativex/reachability-metadata-schema-v1.0.0.json @@ -0,0 +1,362 @@ +{ + "$schema": "https://json-schema.org/draft/2019-09/schema", + "$id": "https://www.graalvm.org/reachability-metadata-schema-v1.0.0.json", + "title": "JSON schema for the reachability metadata used by GraalVM Native Image", + "type": "object", + "default": {}, + "properties": { + "comment": { + "title": "A comment applying to the whole file (e.g., generation date, author, etc.)", + "oneOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "string" + } + } + ], + "default": "" + }, + "reflection": { + "title": "Metadata to ensure elements are reachable through reflection", + "$ref": "#/$defs/reflection" + }, + "jni": { + "title": "Metadata to ensure elements are reachable through JNI", + "$ref": "#/$defs/reflection" + }, + "serialization": { + "title": "Metadata for types that are serialized or deserialized at run time. The types must extend 'java.io.Serializable'.", + "type": "array", + "default": [], + "items": { + "title": "Enables serializing and deserializing objects of the class specified by ", + "type": "object", + "properties": { + "reason": { + "title": "Reason for the type's inclusion in the serialization metadata", + "$ref": "#/$defs/reason" + }, + "condition": { + "title": "Condition under which the class should be registered for serialization", + "$ref": "#/$defs/condition" + }, + "type": { + "title": "Type descriptor of the class that should be registered for serialization", + "$ref": "#/$defs/type" + }, + "customTargetConstructorClass": { + "title": "Fully qualified name of the class whose constructor should be used to serialize the class specified by ", + "type": "string" + } + }, + "required": [ + "type" + ], + "additionalProperties": false + } + }, + "resources": { + "title": "Metadata to ensure resources are available", + "type": "array", + "default": [], + "items": { + "title": "Resource that should be available", + "type": "object", + "properties": { + "reason": { + "title": "Reason for the resource's inclusion in the metadata", + "$ref": "#/$defs/reason" + }, + "condition": { + "title": "Condition under which the resource should be registered for runtime access", + "$ref": "#/$defs/condition" + }, + "module": { + "title": "Module containing the resource", + "type": "string", + "default": "" + }, + "glob": { + "title": "Resource name or pattern matching multiple resources (accepts * and ** wildcards)", + "type": "string" + } + }, + "required": [ + "glob" + ], + "additionalProperties": false + } + }, + "bundles": { + "title": "Metadata to ensure resource bundles are available", + "type": "array", + "default": [], + "items": { + "title": "Resource bundle that should be available", + "type": "object", + "properties": { + "reason": { + "title": "Reason for the resource bundle's inclusion in the metadata", + "$ref": "#/$defs/reason" + }, + "condition": { + "title": "Condition under which the resource bundle should be registered for runtime access", + "$ref": "#/$defs/condition" + }, + "name": { + "title": "Name of the resource bundle", + "type": "string" + }, + "locales": { + "title": "List of locales that should be registered for this resource bundle", + "type": "array", + "default": [], + "items": { + "type": "string" + } + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + } + }, + "required": [], + "additionalProperties": false, + + "$defs": { + "reflection": { + "type": "array", + "default": [], + "items": { + "title": "Elements that should be registered for reflection for a specified type", + "type": "object", + "properties": { + "reason": { + "title": "Reason for the element's inclusion", + "$ref": "#/$defs/reason" + }, + "condition": { + "title": "Condition under which the class should be registered for reflection", + "$ref": "#/$defs/condition" + }, + "type": { + "title": "Type descriptor of the class that should be registered for reflection", + "$ref": "#/$defs/type" + }, + "methods": { + "title": "List of methods that should be registered for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Method descriptor of the method that should be registered for reflection", + "$ref": "#/$defs/method" + } + }, + "fields": { + "title": "List of class fields that can be read or written to for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Field descriptor of the field that should be registered for reflection", + "$ref": "#/$defs/field" + } + }, + "allDeclaredMethods": { + "title": "Register all declared methods from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allDeclaredFields": { + "title": "Register all declared fields from the type for reflective access", + "type": "boolean", + "default": false + }, + "allDeclaredConstructors": { + "title": "Register all declared constructors from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allPublicMethods": { + "title": "Register all public methods from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "allPublicFields": { + "title": "Register all public fields from the type for reflective access", + "type": "boolean", + "default": false + }, + "allPublicConstructors": { + "title": "Register all public constructors from the type for reflective invocation", + "type": "boolean", + "default": false + }, + "unsafeAllocated": { + "title": "Allow objects of this class to be instantiated with a call to jdk.internal.misc.Unsafe#allocateInstance or JNI's AllocObject", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + }, + "jni": { + "type": "array", + "default": [], + "items": { + "title": "Elements that should be registered for JNI for a specified type", + "type": "object", + "properties": { + "reason": { + "title": "Reason for the element's inclusion", + "$ref": "#/$defs/reason" + }, + "condition": { + "title": "Condition under which the class should be registered for JNI", + "$ref": "#/$defs/condition" + }, + "type": { + "title": "Type descriptor of the class that should be registered for JNI", + "$ref": "#/$defs/type" + }, + "methods": { + "title": "List of methods that should be registered for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Method descriptor of the method that should be registered for JNI", + "$ref": "#/$defs/method" + } + }, + "fields": { + "title": "List of class fields that can be read or written to for the type declared in ", + "type": "array", + "default": [], + "items": { + "title": "Field descriptor of the field that should be registered for JNI", + "$ref": "#/$defs/field" + } + }, + "allDeclaredMethods": { + "title": "Register all declared methods from the type for JNI access", + "type": "boolean", + "default": false + }, + "allDeclaredFields": { + "title": "Register all declared fields from the type for JNI access", + "type": "boolean", + "default": false + }, + "allDeclaredConstructors": { + "title": "Register all declared constructors from the type for JNI access", + "type": "boolean", + "default": false + }, + "allPublicMethods": { + "title": "Register all public methods from the type for JNI access", + "type": "boolean", + "default": false + }, + "allPublicFields": { + "title": "Register all public fields from the type for JNI access", + "type": "boolean", + "default": false + }, + "allPublicConstructors": { + "title": "Register all public constructors from the type for JNI access", + "type": "boolean", + "default": false + } + }, + "additionalProperties": false + } + }, + "reason": { + "type": "string", + "default": [] + }, + "condition": { + "title": "Condition used by GraalVM Native Image metadata files", + "type": "object", + "properties": { + "typeReached": { + "title": "Type descriptor of a class that must be reached in order to enable the corresponding registration", + "$ref": "#/$defs/type" + } + }, + "required": [ + "typeReached" + ], + "additionalProperties": false + }, + "type": { + "title": "Type descriptors used by GraalVM Native Image metadata files", + "oneOf": [ + { + "type": "string" + }, + { + "type": "object", + "properties": { + "proxy": { + "title": "List of interfaces defining the proxy class", + "type": "array", + "default": [], + "items": { + "title": "Fully qualified name of the interface defining the proxy class", + "type": "string" + } + } + }, + "required": [ + "proxy" + ], + "additionalProperties": false + } + ] + }, + "method": { + "title": "Method descriptors used by GraalVM Native Image metadata files", + "type": "object", + "properties": { + "name": { + "title": "Method name that should be registered for this class", + "type": "string" + }, + "parameterTypes": { + "default": [], + "items": { + "title": "List of the method's parameter types", + "type": "string" + }, + "type": "array" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + }, + "field": { + "title": "Field descriptors used by GraalVM Native Image metadata files", + "type": "object", + "properties": { + "name": { + "title": "Name of the field that should be registered for reflection", + "type": "string" + } + }, + "required": [ + "name" + ], + "additionalProperties": false + } + } +} \ No newline at end of file diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java index 961457942908..5020eecac329 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractDecoderTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,7 @@ import java.util.Map; import java.util.function.Consumer; -import io.netty5.buffer.Buffer; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -31,7 +31,6 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; @@ -210,13 +209,7 @@ protected void testDecodeError(Publisher input, ResolvableType outpu Flux flux = Mono.from(input).concatWith(Flux.error(new InputException())); assertThatExceptionOfType(InputException.class).isThrownBy(() -> - this.decoder.decode(flux, outputType, mimeType, hints) - .doOnNext(object -> { - if (object instanceof Buffer buffer) { - buffer.close(); - } - }) - .blockLast(Duration.ofSeconds(5))); + this.decoder.decode(flux, outputType, mimeType, hints).blockLast(Duration.ofSeconds(5))); } /** @@ -233,12 +226,7 @@ protected void testDecodeError(Publisher input, ResolvableType outpu protected void testDecodeCancel(Publisher input, ResolvableType outputType, @Nullable MimeType mimeType, @Nullable Map hints) { - Flux result = this.decoder.decode(input, outputType, mimeType, hints) - .doOnNext(object -> { - if (object instanceof Buffer buffer) { - buffer.close(); - } - }); + Flux result = this.decoder.decode(input, outputType, mimeType, hints); StepVerifier.create(result).expectNextCount(1).thenCancel().verify(); } diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java index fdc5c191c7f0..0127e40fd20b 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/codec/AbstractEncoderTests.java @@ -19,6 +19,7 @@ import java.util.Map; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -29,7 +30,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.testfixture.io.buffer.AbstractLeakCheckingTests; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java index a4cbd1f22ed1..f1404079bb29 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/env/MockPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * a user-provided {@link Properties} object, or if omitted during construction, * the implementation will initialize its own. * - * The {@link #setProperty} and {@link #withProperty} methods are exposed for + *

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

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

    Useful for method chaining and fluent-style use. * @return this {@link MockPropertySource} instance */ public MockPropertySource withProperty(String name, Object value) { diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java index d6ff6b14b86d..81895f5c7f8b 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/io/buffer/AbstractDataBufferAllocatingTests.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,8 +34,6 @@ import io.netty.buffer.PooledByteBufAllocator; import io.netty.buffer.PooledByteBufAllocatorMetric; import io.netty.buffer.UnpooledByteBufAllocator; -import io.netty5.buffer.BufferAllocator; -import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.extension.AfterEachCallback; import org.junit.jupiter.api.extension.RegisterExtension; @@ -48,7 +46,6 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.core.io.buffer.Netty5DataBufferFactory; import org.springframework.core.io.buffer.NettyDataBufferFactory; import static java.nio.charset.StandardCharsets.UTF_8; @@ -65,14 +62,6 @@ */ public abstract class AbstractDataBufferAllocatingTests { - private static BufferAllocator netty5OnHeapUnpooled; - - private static BufferAllocator netty5OffHeapUnpooled; - - private static BufferAllocator netty5OffHeapPooled; - - private static BufferAllocator netty5OnHeapPooled; - private static UnpooledByteBufAllocator netty4OffHeapUnpooled; private static UnpooledByteBufAllocator netty4OnHeapUnpooled; @@ -178,19 +167,6 @@ public static void createAllocators() { netty4OffHeapUnpooled = new UnpooledByteBufAllocator(true); netty4OnHeapPooled = new PooledByteBufAllocator(false, 1, 1, 4096, 4, 0, 0, 0, true); netty4OffHeapPooled = new PooledByteBufAllocator(true, 1, 1, 4096, 4, 0, 0, 0, true); - - netty5OnHeapUnpooled = BufferAllocator.onHeapUnpooled(); - netty5OffHeapUnpooled = BufferAllocator.offHeapUnpooled(); - netty5OnHeapPooled = BufferAllocator.onHeapPooled(); - netty5OffHeapPooled = BufferAllocator.offHeapPooled(); - } - - @AfterAll - static void closeAllocators() { - netty5OnHeapUnpooled.close(); - netty5OffHeapUnpooled.close(); - netty5OnHeapPooled.close(); - netty5OffHeapPooled.close(); } @@ -212,15 +188,6 @@ public static Stream dataBufferFactories() { new NettyDataBufferFactory(netty4OffHeapPooled)), argumentSet("NettyDataBufferFactory - PooledByteBufAllocator - preferDirect = false", new NettyDataBufferFactory(netty4OnHeapPooled)), - // Netty 5 - argumentSet("Netty5DataBufferFactory - BufferAllocator.onHeapUnpooled()", - new Netty5DataBufferFactory(netty5OnHeapUnpooled)), - argumentSet("Netty5DataBufferFactory - BufferAllocator.offHeapUnpooled()", - new Netty5DataBufferFactory(netty5OffHeapUnpooled)), - argumentSet("Netty5DataBufferFactory - BufferAllocator.onHeapPooled()", - new Netty5DataBufferFactory(netty5OnHeapPooled)), - argumentSet("Netty5DataBufferFactory - BufferAllocator.offHeapPooled()", - new Netty5DataBufferFactory(netty5OffHeapPooled)), // Default argumentSet("DefaultDataBufferFactory - preferDirect = true", new DefaultDataBufferFactory(true)), diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/ClassMarkedJSpecifyProcessor.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/ClassMarkedJSpecifyProcessor.java new file mode 100644 index 000000000000..e1af26ac4cee --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/ClassMarkedJSpecifyProcessor.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.core.testfixture.nullness; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.NullUnmarked; +import org.jspecify.annotations.Nullable; + +@NullMarked +public interface ClassMarkedJSpecifyProcessor { + + String process(String unspecified, @Nullable String nullable, @NonNull String nonNull); + + @Nullable String nullableProcess(); + + @NonNull String nonNullProcess(); + + @NullUnmarked + String unmarkedProcess(String unspecified, @Nullable String nullable, @NonNull String nonNull); + +} diff --git a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/CustomNullableProcessor.java similarity index 64% rename from spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java rename to spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/CustomNullableProcessor.java index fb34361664d6..25d6ca8b3b5c 100644 --- a/spring-context-indexer/src/test/java/org/springframework/context/index/sample/cdi/SampleManagedBean.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/CustomNullableProcessor.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. @@ -14,15 +14,10 @@ * limitations under the License. */ -package org.springframework.context.index.sample.cdi; +package org.springframework.core.testfixture.nullness; -import jakarta.annotation.ManagedBean; +public interface CustomNullableProcessor { -/** - * Test candidate for a CDI {@link ManagedBean}. - * - * @author Stephane Nicoll - */ -@ManagedBean -public class SampleManagedBean { + @org.springframework.core.testfixture.nullness.custom.Nullable + String process(@org.springframework.core.testfixture.nullness.custom.Nullable String nullable); } diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/JSpecifyProcessor.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/JSpecifyProcessor.java new file mode 100644 index 000000000000..444d765a3da5 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/JSpecifyProcessor.java @@ -0,0 +1,41 @@ +/* + * Copyright 2002-2025 the original author 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.testfixture.nullness; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.NullMarked; +import org.jspecify.annotations.Nullable; + +public interface JSpecifyProcessor { + + String process(String unspecified, @Nullable String nullable, @NonNull String nonNull); + + @Nullable String nullableProcess(); + + @NonNull String nonNullProcess(); + + @NullMarked + String markedProcess(String unspecified, @Nullable String nullable, @NonNull String nonNull); + + @NullMarked + @Nullable String nullableMarkedProcess(); + + @NullMarked + @NonNull String nonNullMarkedProcess(); + + void voidProcess(); +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/NullnessFields.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/NullnessFields.java new file mode 100644 index 000000000000..47f1445d1b5f --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/NullnessFields.java @@ -0,0 +1,31 @@ +/* + * Copyright 2002-2025 the original author 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.testfixture.nullness; + +public class NullnessFields { + + public String unannotatedField = ""; + + public @org.jspecify.annotations.Nullable String jspecifyNullableField; + + public @org.jspecify.annotations.NonNull String jspecifyNonNullField = ""; + + @org.springframework.core.testfixture.nullness.custom.Nullable + public String customNullableField; + + public int primitiveField = 0; +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/custom/Nullable.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/custom/Nullable.java new file mode 100644 index 000000000000..9834dcc0868d --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/custom/Nullable.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.core.testfixture.nullness.custom; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +@Retention(RetentionPolicy.RUNTIME) +public @interface Nullable { +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/PackageMarkedJSpecifyProcessor.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/PackageMarkedJSpecifyProcessor.java new file mode 100644 index 000000000000..5bf7705ff1ab --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/PackageMarkedJSpecifyProcessor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2025 the original author 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.testfixture.nullness.marked; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public interface PackageMarkedJSpecifyProcessor { + + String process(String unspecified, @Nullable String nullable, @NonNull String nonNull); + + @Nullable String nullableProcess(); + + @NonNull String nonNullProcess(); +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/package-info.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/package-info.java new file mode 100644 index 000000000000..e748cfa8145d --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package org.springframework.core.testfixture.nullness.marked; + +import org.jspecify.annotations.NullMarked; diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/unmarked/PackageUnmarkedJSpecifyProcessor.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/unmarked/PackageUnmarkedJSpecifyProcessor.java new file mode 100644 index 000000000000..9f4adcc9db34 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/unmarked/PackageUnmarkedJSpecifyProcessor.java @@ -0,0 +1,29 @@ +/* + * Copyright 2002-2025 the original author 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.testfixture.nullness.marked.unmarked; + +import org.jspecify.annotations.NonNull; +import org.jspecify.annotations.Nullable; + +public interface PackageUnmarkedJSpecifyProcessor { + + String process(String unspecified, @Nullable String nullable, @NonNull String nonNull); + + @Nullable String nullableProcess(); + + @NonNull String nonNullProcess(); +} diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/unmarked/package-info.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/unmarked/package-info.java new file mode 100644 index 000000000000..63157590dd55 --- /dev/null +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/nullness/marked/unmarked/package-info.java @@ -0,0 +1,4 @@ +@NullUnmarked +package org.springframework.core.testfixture.nullness.marked.unmarked; + +import org.jspecify.annotations.NullUnmarked; diff --git a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java index 307cac0b7817..0fc5aef20fbd 100644 --- a/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java +++ b/spring-core/src/testFixtures/java/org/springframework/core/testfixture/security/TestPrincipal.java @@ -18,7 +18,7 @@ import java.security.Principal; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An implementation of {@link Principal} for testing. diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java index 363506b951f6..55f7344f86f2 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorExecutor.java @@ -16,6 +16,8 @@ package org.springframework.expression; +import org.jspecify.annotations.Nullable; + /** * A {@code ConstructorExecutor} is built by a {@link ConstructorResolver} and * can be cached by the infrastructure to repeat an operation quickly without @@ -49,6 +51,6 @@ public interface ConstructorExecutor { * @throws AccessException if there is a problem executing the constructor or * if this {@code ConstructorExecutor} has become stale */ - TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException; + TypedValue execute(EvaluationContext context, @Nullable Object... arguments) throws AccessException; } diff --git a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java index 3c9c232c3ee1..886644f28189 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/ConstructorResolver.java @@ -18,8 +18,9 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * A constructor resolver attempts to locate a constructor and returns a @@ -50,8 +51,7 @@ public interface ConstructorResolver { * @return a {@code ConstructorExecutor} that can invoke the constructor, * or {@code null} if the constructor cannot be found */ - @Nullable - ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) + @Nullable ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) throws AccessException; } diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java index 0fb89de103e9..64e86bb81c50 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationContext.java @@ -20,7 +20,7 @@ import java.util.List; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Expressions are executed in an evaluation context. It is in this context that @@ -89,8 +89,7 @@ default List getMethodResolvers() { /** * Return a bean resolver that can look up beans by name. */ - @Nullable - BeanResolver getBeanResolver(); + @Nullable BeanResolver getBeanResolver(); /** * Return a type locator that can be used to find types, either by short or @@ -150,8 +149,7 @@ default TypedValue assignVariable(String name, Supplier valueSupplie * @param name the name of the variable to look up * @return the value of the variable, or {@code null} if not found */ - @Nullable - Object lookupVariable(String name); + @Nullable Object lookupVariable(String name); /** * Determine if assignment is enabled within expressions evaluated by this evaluation diff --git a/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java b/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java index ec66a5268294..1ee1d34b00db 100644 --- a/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java +++ b/spring-expression/src/main/java/org/springframework/expression/EvaluationException.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represent an exception that occurs during expression evaluation. diff --git a/spring-expression/src/main/java/org/springframework/expression/Expression.java b/spring-expression/src/main/java/org/springframework/expression/Expression.java index a34daa068e38..4da51d7cb2ea 100644 --- a/spring-expression/src/main/java/org/springframework/expression/Expression.java +++ b/spring-expression/src/main/java/org/springframework/expression/Expression.java @@ -16,8 +16,9 @@ package org.springframework.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * An expression capable of evaluating itself against context objects. @@ -44,8 +45,7 @@ public interface Expression { * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - Object getValue() throws EvaluationException; + @Nullable Object getValue() throws EvaluationException; /** * Evaluate this expression in the default context and return the result of evaluation. @@ -55,8 +55,7 @@ public interface Expression { * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - T getValue(@Nullable Class desiredResultType) throws EvaluationException; + @Nullable T getValue(@Nullable Class desiredResultType) throws EvaluationException; /** * Evaluate this expression in the default context against the specified root object @@ -65,8 +64,7 @@ public interface Expression { * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - Object getValue(@Nullable Object rootObject) throws EvaluationException; + @Nullable Object getValue(@Nullable Object rootObject) throws EvaluationException; /** * Evaluate this expression in the default context against the specified root object @@ -78,8 +76,7 @@ public interface Expression { * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) + @Nullable T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException; /** @@ -88,8 +85,7 @@ T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - Object getValue(EvaluationContext context) throws EvaluationException; + @Nullable Object getValue(EvaluationContext context) throws EvaluationException; /** * Evaluate this expression in the provided context against the specified root object @@ -101,8 +97,7 @@ T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + @Nullable Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; /** * Evaluate this expression in the provided context and return the result of evaluation. @@ -113,8 +108,7 @@ T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - T getValue(EvaluationContext context, @Nullable Class desiredResultType) + @Nullable T getValue(EvaluationContext context, @Nullable Class desiredResultType) throws EvaluationException; /** @@ -130,8 +124,7 @@ T getValue(EvaluationContext context, @Nullable Class desiredResultType) * @return the evaluation result * @throws EvaluationException if there is a problem during evaluation */ - @Nullable - T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) + @Nullable T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException; /** @@ -140,8 +133,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - Class getValueType() throws EvaluationException; + @Nullable Class getValueType() throws EvaluationException; /** * Return the most general type that can be passed to the @@ -150,8 +142,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - Class getValueType(@Nullable Object rootObject) throws EvaluationException; + @Nullable Class getValueType(@Nullable Object rootObject) throws EvaluationException; /** * Return the most general type that can be passed to the @@ -160,8 +151,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - Class getValueType(EvaluationContext context) throws EvaluationException; + @Nullable Class getValueType(EvaluationContext context) throws EvaluationException; /** * Return the most general type that can be passed to the @@ -173,8 +163,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return the most general type of value that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; + @Nullable Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; /** * Return a descriptor for the most general type that can be passed to one of @@ -182,8 +171,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - TypeDescriptor getValueTypeDescriptor() throws EvaluationException; + @Nullable TypeDescriptor getValueTypeDescriptor() throws EvaluationException; /** * Return a descriptor for the most general type that can be passed to the @@ -192,8 +180,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException; + @Nullable TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException; /** * Return a descriptor for the most general type that can be passed to the @@ -202,8 +189,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException; + @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException; /** * Return a descriptor for the most general type that can be passed to the @@ -216,8 +202,7 @@ T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable * @return a type descriptor for values that can be set in this context * @throws EvaluationException if there is a problem determining the type */ - @Nullable - TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) + @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException; /** diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java index 925a18e05bd4..7ee2ae06d4f0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionException.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Superclass for exceptions that can occur whilst processing expressions. @@ -28,8 +28,7 @@ @SuppressWarnings("serial") public class ExpressionException extends RuntimeException { - @Nullable - protected final String expressionString; + protected final @Nullable String expressionString; protected int position; // -1 if not known; should be known in all reasonable cases @@ -105,8 +104,7 @@ public ExpressionException(int position, String message, @Nullable Throwable cau /** * Return the expression string. */ - @Nullable - public final String getExpressionString() { + public final @Nullable String getExpressionString() { return this.expressionString; } @@ -155,7 +153,7 @@ public String toDetailedString() { * that caused the failure. * @since 4.0 */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public String getSimpleMessage() { return super.getMessage(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java b/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java index 1cb24e5d1672..87000d450abe 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java +++ b/spring-expression/src/main/java/org/springframework/expression/ExpressionInvocationTargetException.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * This exception wraps (as cause) a checked exception thrown by some method that SpEL diff --git a/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java b/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java index 1471ad48df35..adf4c3db5f7e 100644 --- a/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/IndexAccessor.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An index accessor is able to read from and possibly write to an indexed diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java index 6d717cd4b008..cde52a2b7848 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodExecutor.java @@ -16,6 +16,8 @@ package org.springframework.expression; +import org.jspecify.annotations.Nullable; + /** * A {@code MethodExecutor} is built by a {@link MethodResolver} and can be cached * by the infrastructure to repeat an operation quickly without going back to the @@ -50,6 +52,6 @@ public interface MethodExecutor { * @throws AccessException if there is a problem executing the method or * if this {@code MethodExecutor} has become stale */ - TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException; + TypedValue execute(EvaluationContext context, Object target, @Nullable Object... arguments) throws AccessException; } diff --git a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java index 9fb9137c8e4e..314159311af7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/MethodResolver.java @@ -18,8 +18,9 @@ import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * A method resolver attempts to locate a method and returns a @@ -50,8 +51,7 @@ public interface MethodResolver { * @return a {@code MethodExecutor} that can invoke the method, or {@code null} * if the method cannot be found */ - @Nullable - MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + @Nullable MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List argumentTypes) throws AccessException; } diff --git a/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java index 8bece1f0fefc..3c6d36d68144 100644 --- a/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java +++ b/spring-expression/src/main/java/org/springframework/expression/OperatorOverloader.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * By default, the mathematical operators defined in {@link Operation} support simple diff --git a/spring-expression/src/main/java/org/springframework/expression/ParseException.java b/spring-expression/src/main/java/org/springframework/expression/ParseException.java index 3c85016d1657..ed23f06bf815 100644 --- a/spring-expression/src/main/java/org/springframework/expression/ParseException.java +++ b/spring-expression/src/main/java/org/springframework/expression/ParseException.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represent an exception that occurs during expression parsing. diff --git a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java index 84e4fe5ac1dd..5dd20f33b0f0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/PropertyAccessor.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A property accessor is able to read from (and possibly write to) an object's diff --git a/spring-expression/src/main/java/org/springframework/expression/TargetedAccessor.java b/spring-expression/src/main/java/org/springframework/expression/TargetedAccessor.java index 6403f9ca07ba..556036cf17c9 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TargetedAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/TargetedAccessor.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy for types that access elements of specific target classes. @@ -52,7 +52,6 @@ public interface TargetedAccessor { * @return an array of classes that this accessor is suitable for * (or {@code null} or an empty array if a generic accessor) */ - @Nullable - Class[] getSpecificTargetClasses(); + Class @Nullable [] getSpecificTargetClasses(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java b/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java index 88d07d39fb8e..d940e0f5c8e8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java +++ b/spring-expression/src/main/java/org/springframework/expression/TypeComparator.java @@ -16,7 +16,7 @@ package org.springframework.expression; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Instances of a type comparator should be able to compare pairs of objects for equality. diff --git a/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java b/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java index 6974d6b8747c..7e83e8eff59a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java +++ b/spring-expression/src/main/java/org/springframework/expression/TypeConverter.java @@ -16,8 +16,9 @@ package org.springframework.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * A type converter can convert values between different types encountered during @@ -54,7 +55,6 @@ public interface TypeConverter { * @return the converted value * @throws EvaluationException if conversion failed or is not possible to begin with */ - @Nullable - Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); + @Nullable Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType); } diff --git a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java index b97020a259ec..204c68a34008 100644 --- a/spring-expression/src/main/java/org/springframework/expression/TypedValue.java +++ b/spring-expression/src/main/java/org/springframework/expression/TypedValue.java @@ -16,8 +16,9 @@ package org.springframework.expression; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** @@ -38,11 +39,9 @@ public class TypedValue { public static final TypedValue NULL = new TypedValue(null); - @Nullable - private final Object value; + private final @Nullable Object value; - @Nullable - private TypeDescriptor typeDescriptor; + private @Nullable TypeDescriptor typeDescriptor; /** @@ -67,13 +66,11 @@ public TypedValue(@Nullable Object value, @Nullable TypeDescriptor typeDescripto } - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } - @Nullable - public TypeDescriptor getTypeDescriptor() { + public @Nullable TypeDescriptor getTypeDescriptor() { if (this.typeDescriptor == null && this.value != null) { this.typeDescriptor = TypeDescriptor.forObject(this.value); } diff --git a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java index 8bb6dcf195ce..0b1349720aa4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/CompositeStringExpression.java @@ -16,12 +16,13 @@ package org.springframework.expression.common; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; /** * Represents a template expression broken into pieces. @@ -78,8 +79,7 @@ public String getValue() throws EvaluationException { } @Override - @Nullable - public T getValue(@Nullable Class expectedResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Class expectedResultType) throws EvaluationException { String value = getValue(); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); } @@ -97,8 +97,7 @@ public String getValue(@Nullable Object rootObject) throws EvaluationException { } @Override - @Nullable - public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { String value = getValue(rootObject); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); } @@ -116,8 +115,7 @@ public String getValue(EvaluationContext context) throws EvaluationException { } @Override - @Nullable - public T getValue(EvaluationContext context, @Nullable Class expectedResultType) + public @Nullable T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { String value = getValue(context); @@ -137,8 +135,7 @@ public String getValue(EvaluationContext context, @Nullable Object rootObject) t } @Override - @Nullable - public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) + public @Nullable T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { String value = getValue(context,rootObject); diff --git a/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java b/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java index 3ea264baf769..9489f9f3bc9a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/ExpressionUtils.java @@ -16,12 +16,13 @@ package org.springframework.expression.common; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeConverter; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -45,8 +46,7 @@ public abstract class ExpressionUtils { * of the value to the specified type is not supported */ @SuppressWarnings("unchecked") - @Nullable - public static T convertTypedValue( + public static @Nullable T convertTypedValue( @Nullable EvaluationContext context, TypedValue typedValue, @Nullable Class targetType) { Object value = typedValue.getValue(); diff --git a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java index ca9275ad214c..3876b0488bd7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/LiteralExpression.java @@ -16,12 +16,13 @@ package org.springframework.expression.common; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; import org.springframework.expression.Expression; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; /** * A very simple, hard-coded implementation of the {@link Expression} interface @@ -62,8 +63,7 @@ public String getValue() { } @Override - @Nullable - public T getValue(@Nullable Class expectedResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Class expectedResultType) throws EvaluationException { String value = getValue(); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), expectedResultType); } @@ -74,8 +74,7 @@ public String getValue(@Nullable Object rootObject) { } @Override - @Nullable - public T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { String value = getValue(rootObject); return ExpressionUtils.convertTypedValue(null, new TypedValue(value), desiredResultType); } @@ -86,8 +85,7 @@ public String getValue(EvaluationContext context) { } @Override - @Nullable - public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { + public @Nullable T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { String value = getValue(context); return ExpressionUtils.convertTypedValue(context, new TypedValue(value), expectedResultType); } @@ -98,8 +96,7 @@ public String getValue(EvaluationContext context, @Nullable Object rootObject) t } @Override - @Nullable - public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) + public @Nullable T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class desiredResultType) throws EvaluationException { String value = getValue(context, rootObject); diff --git a/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java index bdf4ad1660d5..967950551bf3 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/TemplateAwareExpressionParser.java @@ -21,11 +21,12 @@ import java.util.Deque; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-expression/src/main/java/org/springframework/expression/common/package-info.java b/spring-expression/src/main/java/org/springframework/expression/common/package-info.java index 080af63ff99b..f91c56608f67 100644 --- a/spring-expression/src/main/java/org/springframework/expression/common/package-info.java +++ b/spring-expression/src/main/java/org/springframework/expression/common/package-info.java @@ -1,9 +1,7 @@ /** * Common utility classes behind the Spring Expression Language. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.expression.common; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-expression/src/main/java/org/springframework/expression/package-info.java b/spring-expression/src/main/java/org/springframework/expression/package-info.java index 48a91d155356..bd9caff6139c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/package-info.java +++ b/spring-expression/src/main/java/org/springframework/expression/package-info.java @@ -1,9 +1,7 @@ /** * Core abstractions behind the Spring Expression Language. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.expression; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 85e9bb61db11..43bd339ebe2c 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 @@ -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,11 +23,12 @@ import java.util.Deque; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.ClassWriter; import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -69,8 +70,7 @@ public class CodeFlow implements Opcodes { * they can register to add a field to this class. Any registered FieldAdders * will be called after the main evaluation function has finished being generated. */ - @Nullable - private List fieldAdders; + private @Nullable List fieldAdders; /** * As SpEL AST nodes are called to generate code for the main evaluation method @@ -78,8 +78,7 @@ public class CodeFlow implements Opcodes { * registered ClinitAdders will be called after the main evaluation function * has finished being generated. */ - @Nullable - private List clinitAdders; + private @Nullable List clinitAdders; /** * When code generation requires holding a value in a class level field, this @@ -157,8 +156,7 @@ public void exitCompilationScope() { /** * Return the descriptor for the item currently on top of the stack (in the current scope). */ - @Nullable - public String lastDescriptor() { + public @Nullable String lastDescriptor() { return CollectionUtils.lastElement(this.compilationScopes.peek()); } @@ -518,11 +516,9 @@ public static String createSignatureDescriptor(Constructor ctor) { */ public static String toJvmDescriptor(Class clazz) { StringBuilder sb = new StringBuilder(); - if (clazz.isArray()) { - while (clazz.isArray()) { - sb.append('['); - clazz = clazz.componentType(); - } + while (clazz.isArray()) { + sb.append('['); + clazz = clazz.componentType(); } if (clazz.isPrimitive()) { if (clazz == boolean.class) { @@ -691,9 +687,8 @@ public static boolean isPrimitiveOrUnboxableSupportedNumber(@Nullable String des * 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/main/java/org/springframework/expression/spel/CompiledExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java index acab38946db5..27eb79d2a3e8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/CompiledExpression.java @@ -16,9 +16,10 @@ package org.springframework.expression.spel; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; -import org.springframework.lang.Nullable; /** * Base superclass for compiled expressions. Each generated compiled expression class diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java index 32247c95b949..b8bde3014988 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ExpressionState.java @@ -18,12 +18,12 @@ import java.util.ArrayDeque; import java.util.Deque; -import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.NoSuchElementException; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; @@ -33,7 +33,6 @@ import org.springframework.expression.TypeComparator; import org.springframework.expression.TypeConverter; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -61,11 +60,7 @@ public class ExpressionState { private final SpelParserConfiguration configuration; - @Nullable - private Deque contextObjects; - - @Nullable - private Deque variableScopes; + private @Nullable Deque contextObjects; // When entering a new scope there is a new base object which should be used // for '#this' references (or to act as a target for unqualified references). @@ -74,8 +69,7 @@ public class ExpressionState { // #list1.?[#list2.contains(#this)] // On entering the selection we enter a new scope, and #this is now the // element from list1. - @Nullable - private Deque scopeRootObjects; + private @Nullable Deque scopeRootObjects; public ExpressionState(EvaluationContext context) { @@ -195,8 +189,7 @@ public Object convertValue(Object value, TypeDescriptor targetTypeDescriptor) th return result; } - @Nullable - public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { + public @Nullable Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor) throws EvaluationException { Object val = value.getValue(); return this.relatedContext.getTypeConverter().convertValue( val, TypeDescriptor.forObject(val), targetTypeDescriptor); @@ -207,73 +200,13 @@ public Object convertValue(TypedValue value, TypeDescriptor targetTypeDescriptor * context object} and a new local variable scope. */ public void enterScope() { - initVariableScopes().push(new VariableScope()); - initScopeRootObjects().push(getActiveContextObject()); - } - - /** - * Enter a new scope with a new {@linkplain #getActiveContextObject() root - * context object} and a new local variable scope containing the supplied - * name/value pair. - * @param name the name of the local variable - * @param value the value of the local variable - * @deprecated as of 6.2 with no replacement; to be removed in 7.0 - */ - @Deprecated(since = "6.2", forRemoval = true) - public void enterScope(String name, Object value) { - initVariableScopes().push(new VariableScope(name, value)); - initScopeRootObjects().push(getActiveContextObject()); - } - - /** - * Enter a new scope with a new {@linkplain #getActiveContextObject() root - * context object} and a new local variable scope containing the supplied - * name/value pairs. - * @param variables a map containing name/value pairs for local variables - * @deprecated as of 6.2 with no replacement; to be removed in 7.0 - */ - @Deprecated(since = "6.2", forRemoval = true) - public void enterScope(@Nullable Map variables) { - initVariableScopes().push(new VariableScope(variables)); initScopeRootObjects().push(getActiveContextObject()); } public void exitScope() { - initVariableScopes().pop(); initScopeRootObjects().pop(); } - /** - * Set a local variable with the given name to the supplied value within the - * current scope. - *

    If a local variable with the given name already exists, it will be - * overwritten. - * @param name the name of the local variable - * @param value the value of the local variable - * @deprecated as of 6.2 with no replacement; to be removed in 7.0 - */ - @Deprecated(since = "6.2", forRemoval = true) - public void setLocalVariable(String name, Object value) { - initVariableScopes().element().setVariable(name, value); - } - - /** - * Look up the value of the local variable with the given name. - * @param name the name of the local variable - * @return the value of the local variable, or {@code null} if the variable - * does not exist in the current scope - * @deprecated as of 6.2 with no replacement; to be removed in 7.0 - */ - @Deprecated(since = "6.2", forRemoval = true) - @Nullable - public Object lookupLocalVariable(String name) { - for (VariableScope scope : initVariableScopes()) { - if (scope.definesVariable(name)) { - return scope.lookupVariable(name); - } - } - return null; - } private Deque initContextObjects() { if (this.contextObjects == null) { @@ -289,15 +222,6 @@ private Deque initScopeRootObjects() { return this.scopeRootObjects; } - private Deque initVariableScopes() { - if (this.variableScopes == null) { - this.variableScopes = new ArrayDeque<>(); - // top-level empty variable scope - this.variableScopes.add(new VariableScope()); - } - return this.variableScopes; - } - public TypedValue operate(Operation op, @Nullable Object left, @Nullable Object right) throws EvaluationException { OperatorOverloader overloader = this.relatedContext.getOperatorOverloader(); if (overloader.overridesOperation(op, left, right)) { @@ -323,43 +247,4 @@ public SpelParserConfiguration getConfiguration() { return this.configuration; } - - /** - * A new local variable scope is entered when a new expression scope is - * entered and exited when the corresponding expression scope is exited. - * - *

    If variable names clash with those in a higher level scope, those in - * the higher level scope will not be accessible within the current scope. - */ - private static class VariableScope { - - private final Map variables = new HashMap<>(); - - VariableScope() { - } - - VariableScope(String name, Object value) { - this.variables.put(name, value); - } - - VariableScope(@Nullable Map variables) { - if (variables != null) { - this.variables.putAll(variables); - } - } - - @Nullable - Object lookupVariable(String name) { - return this.variables.get(name); - } - - void setVariable(String name, Object value) { - this.variables.put(name,value); - } - - boolean definesVariable(String name) { - return this.variables.containsKey(name); - } - } - } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java b/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java index d6e173c5f140..434192b36558 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/InternalParseException.java @@ -16,7 +16,7 @@ package org.springframework.expression.spel; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Wraps a real parse exception. This exception flows to the top parse method and then @@ -33,8 +33,7 @@ public InternalParseException(@Nullable SpelParseException cause) { } @Override - @Nullable - public SpelParseException getCause() { + public @Nullable SpelParseException getCause() { return (SpelParseException) super.getCause(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java index 9817ba413490..cc9e53377162 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelEvaluationException.java @@ -16,8 +16,9 @@ package org.springframework.expression.spel; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationException; -import org.springframework.lang.Nullable; /** * Root exception for Spring EL related exceptions. @@ -36,8 +37,7 @@ public class SpelEvaluationException extends EvaluationException { private final SpelMessage message; - @Nullable - private final Object[] inserts; + private final @Nullable Object[] inserts; public SpelEvaluationException(SpelMessage message, @Nullable Object... inserts) { @@ -82,8 +82,7 @@ public SpelMessage getMessageCode() { /** * Return the message inserts. */ - @Nullable - public Object[] getInserts() { + public @Nullable Object @Nullable [] getInserts() { return this.inserts; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java index 03c7fd131243..3808a89f9afc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelMessage.java @@ -18,7 +18,7 @@ import java.text.MessageFormat; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Contains all the messages that can be produced by the Spring Expression Language. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java index 3e2036d031ee..bf4bc8c65b1b 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelNode.java @@ -16,10 +16,11 @@ package org.springframework.expression.spel; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; /** * Represents a node in the abstract syntax tree (AST) for a parsed Spring @@ -38,8 +39,7 @@ public interface SpelNode { * @return the value of this node evaluated against the specified state * @throws EvaluationException if any problem occurs evaluating the expression */ - @Nullable - Object getValue(ExpressionState expressionState) throws EvaluationException; + @Nullable Object getValue(ExpressionState expressionState) throws EvaluationException; /** * Evaluate the expression node in the context of the supplied expression state @@ -96,8 +96,7 @@ public interface SpelNode { * @return the class of the object if it is not already a class object, * or {@code null} if the object is {@code null} */ - @Nullable - Class getObjectClass(@Nullable Object obj); + @Nullable Class getObjectClass(@Nullable Object obj); /** * Return the start position of this AST node in the expression string. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java index 4b5e4c5010db..7ec863a489a7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParseException.java @@ -16,8 +16,9 @@ package org.springframework.expression.spel; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.ParseException; -import org.springframework.lang.Nullable; /** * Root exception for Spring EL related exceptions. Rather than holding a hard coded diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java index 27b6ccff35fb..701c72cbdd93 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/SpelParserConfiguration.java @@ -18,8 +18,9 @@ import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.core.SpringProperties; -import org.springframework.lang.Nullable; /** * Configuration object for the SpEL expression parser. @@ -54,8 +55,7 @@ public class SpelParserConfiguration { private final SpelCompilerMode compilerMode; - @Nullable - private final ClassLoader compilerClassLoader; + private final @Nullable ClassLoader compilerClassLoader; private final boolean autoGrowNullReferences; @@ -150,8 +150,7 @@ public SpelCompilerMode getCompilerMode() { /** * Return the ClassLoader to use as the basis for expression compilation. */ - @Nullable - public ClassLoader getCompilerClassLoader() { + public @Nullable ClassLoader getCompilerClassLoader() { return this.compilerClassLoader; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AccessorUtils.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AccessorUtils.java index fcb03ced3b84..78ef30847d54 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/AccessorUtils.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/AccessorUtils.java @@ -20,8 +20,9 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.TargetedAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java index e4253f7eee8f..ba8d5ba74a91 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ConstructorReference.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,8 @@ import java.util.List; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; @@ -39,7 +41,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.ReflectiveConstructorExecutor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -73,12 +74,10 @@ public class ConstructorReference extends SpelNodeImpl { private final boolean isArrayConstructor; - @Nullable - private final SpelNodeImpl[] dimensions; + private final SpelNodeImpl @Nullable [] dimensions; /** The cached executor that may be reused on subsequent evaluations. */ - @Nullable - private volatile ConstructorExecutor cachedExecutor; + private volatile @Nullable ConstructorExecutor cachedExecutor; /** @@ -125,7 +124,7 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep * @throws EvaluationException if there is a problem creating the object */ private TypedValue createNewInstance(ExpressionState state) throws EvaluationException { - Object[] arguments = new Object[getChildCount() - 1]; + @Nullable Object[] arguments = new Object[getChildCount() - 1]; List argumentTypes = new ArrayList<>(getChildCount() - 1); for (int i = 0; i < arguments.length; i++) { TypedValue childValue = this.children[i + 1].getValueInternal(state); @@ -360,7 +359,7 @@ private void checkNumElements(long numElements) { private Object createReferenceTypeArray(ExpressionState state, TypeConverter typeConverter, SpelNodeImpl[] children, Class componentType) { - Object[] array = (Object[]) Array.newInstance(componentType, children.length); + @Nullable Object[] array = (Object[]) Array.newInstance(componentType, children.length); TypeDescriptor targetType = TypeDescriptor.valueOf(componentType); for (int i = 0; i < array.length; i++) { Object value = children[i].getValue(state); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java index 6f6c26f8f29e..f6c8fbbf374a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Elvis.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. @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import java.util.Optional; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; @@ -26,9 +28,13 @@ import org.springframework.util.ObjectUtils; /** - * Represents the Elvis operator ?:. For an expression a?:b if a is neither null - * nor an empty String, the value of the expression is a. - * If a is null or the empty String, then the value of the expression is b. + * Represents the Elvis operator {@code ?:}. + * + *

    For the expression "{@code A ?: B}", if {@code A} is neither {@code null}, + * an empty {@link Optional}, nor an empty {@link String}, the value of the + * expression is {@code A}, or {@code A.get()} for an {@code Optional}. If + * {@code A} is {@code null}, an empty {@code Optional}, or an + * empty {@code String}, the value of the expression is {@code B}. * * @author Andy Clement * @author Juergen Hoeller @@ -43,18 +49,32 @@ public Elvis(int startPos, int endPos, SpelNodeImpl... args) { /** - * Evaluate the condition and if neither null nor an empty String, return it. - * If it is null or an empty String, return the other value. + * If the left-hand operand is neither neither {@code null}, an empty + * {@link Optional}, nor an empty {@link String}, return its value, or the + * value contained in the {@code Optional}. If the left-hand operand is + * {@code null}, an empty {@code Optional}, or an empty {@code String}, + * return the other value. * @param state the expression state - * @throws EvaluationException if the condition does not evaluate correctly - * to a boolean or there is a problem executing the chosen alternative + * @throws EvaluationException if the null/empty check does not evaluate correctly + * or there is a problem evaluating the alternative */ @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { - TypedValue value = this.children[0].getValueInternal(state); + TypedValue leftHandTypedValue = this.children[0].getValueInternal(state); + Object leftHandValue = leftHandTypedValue.getValue(); + + if (leftHandValue instanceof Optional optional) { + // Compilation is currently not supported for Optional with the Elvis operator. + this.exitTypeDescriptor = null; + if (optional.isPresent()) { + return new TypedValue(optional.get()); + } + return this.children[1].getValueInternal(state); + } + // If this check is changed, the generateCode method will need changing too - if (value.getValue() != null && !"".equals(value.getValue())) { - return value; + if (leftHandValue != null && !"".equals(leftHandValue)) { + return leftHandTypedValue; } else { TypedValue result = this.children[1].getValueInternal(state); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java index c05cfad67435..a3d5a2dad3c1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FormatHelper.java @@ -19,8 +19,9 @@ import java.util.List; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; /** * Utility methods (formatters, etc) used during parsing and evaluation. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java index 20ee8e20b597..acd2d129dbdc 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/FunctionReference.java @@ -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,8 @@ import java.lang.reflect.Modifier; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; @@ -34,7 +36,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.ReflectionHelper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -62,8 +63,7 @@ public class FunctionReference extends SpelNodeImpl { // Captures the most recently used method for the function invocation *if* the method // can safely be used for compilation (i.e. no argument conversion is going on) - @Nullable - private volatile Method method; + private volatile @Nullable Method method; public FunctionReference(String functionName, int startPos, int endPos, SpelNodeImpl... arguments) { @@ -116,7 +116,7 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep * @throws EvaluationException if there is any problem invoking the method */ private TypedValue executeFunctionViaMethod(ExpressionState state, Method method) throws EvaluationException { - Object[] functionArgs = getArguments(state); + @Nullable Object[] functionArgs = getArguments(state); if (!method.isVarArgs()) { int declaredParamCount = method.getParameterCount(); @@ -175,7 +175,7 @@ private TypedValue executeFunctionViaMethod(ExpressionState state, Method method * @since 6.1 */ private TypedValue executeFunctionViaMethodHandle(ExpressionState state, MethodHandle methodHandle) throws EvaluationException { - Object[] functionArgs = getArguments(state); + @Nullable Object[] functionArgs = getArguments(state); MethodType declaredParams = methodHandle.type(); int spelParamCount = functionArgs.length; int declaredParamCount = declaredParams.parameterCount(); @@ -280,9 +280,9 @@ public String toStringAST() { * Compute the arguments to the function, they are the children of this expression node. * @return an array of argument values for the function call */ - private Object[] getArguments(ExpressionState state) throws EvaluationException { + private @Nullable Object[] getArguments(ExpressionState state) throws EvaluationException { // Compute arguments to the function - Object[] arguments = new Object[getChildCount()]; + @Nullable Object[] arguments = new Object[getChildCount()]; for (int i = 0; i < arguments.length; i++) { arguments[i] = this.children[i].getValueInternal(state).getValue(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java index 94cc5d37016d..850e2c02a7c0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Indexer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,8 +20,11 @@ import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; @@ -39,7 +42,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -66,7 +68,12 @@ *

    As of Spring Framework 6.2, null-safe indexing is supported via the {@code '?.'} * operator. For example, {@code 'colors?.[0]'} will evaluate to {@code null} if * {@code colors} is {@code null} and will otherwise evaluate to the 0th - * color. + * color. As of Spring Framework 7.0, null-safe indexing also applies when + * indexing into a structure contained in an {@link Optional}. For example, if + * {@code colors} is of type {@code Optional}, the expression + * {@code 'colors?.[0]'} will evaluate to {@code null} if {@code colors} is + * {@code null} or {@link Optional#isEmpty() empty} and will otherwise evaluate + * to the 0th color, effectively {@code colors.get()[0]}. * * @author Andy Clement * @author Phillip Webb @@ -101,38 +108,20 @@ private AccessMode(boolean supportsReads, boolean supportsWrites) { private final boolean nullSafe; - @Nullable - private IndexedType indexedType; - - @Nullable - private volatile String originalPrimitiveExitTypeDescriptor; + private @Nullable IndexedType indexedType; - @Nullable - private volatile String arrayTypeDescriptor; + private volatile @Nullable String originalPrimitiveExitTypeDescriptor; - @Nullable - private volatile CachedPropertyState cachedPropertyReadState; + private volatile @Nullable String arrayTypeDescriptor; - @Nullable - private volatile CachedPropertyState cachedPropertyWriteState; + private volatile @Nullable CachedPropertyState cachedPropertyReadState; - @Nullable - private volatile CachedIndexState cachedIndexReadState; + private volatile @Nullable CachedPropertyState cachedPropertyWriteState; - @Nullable - private volatile CachedIndexState cachedIndexWriteState; + private volatile @Nullable CachedIndexState cachedIndexReadState; + private volatile @Nullable CachedIndexState cachedIndexWriteState; - /** - * Create an {@code Indexer} with the given start position, end position, and - * index expression. - * @see #Indexer(boolean, int, int, SpelNodeImpl) - * @deprecated as of 6.2, in favor of {@link #Indexer(boolean, int, int, SpelNodeImpl)} - */ - @Deprecated(since = "6.2", forRemoval = true) - public Indexer(int startPos, int endPos, SpelNodeImpl indexExpression) { - this(false, startPos, endPos, indexExpression); - } /** * Create an {@code Indexer} with the given null-safe flag, start position, @@ -182,11 +171,20 @@ private ValueRef getValueRef(ExpressionState state, AccessMode accessMode) throw TypedValue context = state.getActiveContextObject(); Object target = context.getValue(); - if (target == null) { - if (this.nullSafe) { + if (isNullSafe()) { + if (target == null) { return ValueRef.NullValueRef.INSTANCE; } - // Raise a proper exception in case of a null target + if (target instanceof Optional optional) { + if (optional.isEmpty()) { + return ValueRef.NullValueRef.INSTANCE; + } + target = optional.get(); + } + } + + // Raise a proper exception in case of a null target + if (target == null) { throw new SpelEvaluationException(getStartPosition(), SpelMessage.CANNOT_INDEX_INTO_NULL_VALUE); } @@ -347,7 +345,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { } Label skipIfNull = null; - if (this.nullSafe) { + if (isNullSafe()) { mv.visitInsn(DUP); skipIfNull = new Label(); Label continueLabel = new Label(); @@ -456,7 +454,7 @@ private void setExitTypeDescriptor(String descriptor) { // If this indexer would return a primitive - and yet it is also marked // null-safe - then the exit type descriptor must be promoted to the box // type to allow a null value to be passed on. - if (this.nullSafe && CodeFlow.isPrimitive(descriptor)) { + if (isNullSafe() && CodeFlow.isPrimitive(descriptor)) { this.originalPrimitiveExitTypeDescriptor = descriptor; this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); } @@ -680,8 +678,7 @@ private class MapIndexingValueRef implements ValueRef { private final Map map; - @Nullable - private final Object key; + private final @Nullable Object key; private final TypeDescriptor mapEntryDescriptor; @@ -916,8 +913,7 @@ public boolean isWritable() { return (this.collection instanceof List); } - @Nullable - private static Constructor getDefaultConstructor(Class type) { + private static @Nullable Constructor getDefaultConstructor(Class type) { try { return ReflectionUtils.accessibleConstructor(type); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java index 4498d9f6a5de..5a2cc0c6f856 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; @@ -28,7 +30,6 @@ import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,8 +43,7 @@ */ public class InlineList extends SpelNodeImpl { - @Nullable - private final TypedValue constant; + private final @Nullable TypedValue constant; public InlineList(int startPos, int endPos, SpelNodeImpl... args) { @@ -58,8 +58,7 @@ public InlineList(int startPos, int endPos, SpelNodeImpl... args) { *

    This will speed up later getValue calls and reduce the amount of garbage * created. */ - @Nullable - private TypedValue computeConstantValue() { + private @Nullable TypedValue computeConstantValue() { for (int c = 0, max = getChildCount(); c < max; c++) { SpelNode child = getChild(c); if (!(child instanceof Literal)) { @@ -125,8 +124,7 @@ public boolean isConstant() { } @SuppressWarnings("unchecked") - @Nullable - public List getConstantValue() { + public @Nullable List getConstantValue() { Assert.state(this.constant != null, "No constant"); return (List) this.constant.getValue(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java index b9450134f24a..abffdd0996ed 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineMap.java @@ -20,12 +20,13 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelNode; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,8 +40,7 @@ */ public class InlineMap extends SpelNodeImpl { - @Nullable - private final TypedValue constant; + private final @Nullable TypedValue constant; public InlineMap(int startPos, int endPos, SpelNodeImpl... args) { @@ -55,8 +55,7 @@ public InlineMap(int startPos, int endPos, SpelNodeImpl... args) { *

    This will speed up later getValue calls and reduce the amount of garbage * created. */ - @Nullable - private TypedValue computeConstantValue() { + private @Nullable TypedValue computeConstantValue() { for (int c = 0, max = getChildCount(); c < max; c++) { SpelNode child = getChild(c); if (!(child instanceof Literal)) { @@ -163,8 +162,7 @@ public boolean isConstant() { } @SuppressWarnings("unchecked") - @Nullable - public Map getConstantValue() { + public @Nullable Map getConstantValue() { Assert.state(this.constant != null, "No constant"); return (Map) this.constant.getValue(); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java index dbd9e0f2836b..abe35c6cc92c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Literal.java @@ -16,13 +16,14 @@ package org.springframework.expression.spel.ast; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.InternalParseException; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelParseException; -import org.springframework.lang.Nullable; /** * Common superclass for nodes representing literals (boolean, string, number, etc). @@ -33,8 +34,7 @@ */ public abstract class Literal extends SpelNodeImpl { - @Nullable - private final String originalValue; + private final @Nullable String originalValue; public Literal(@Nullable String originalValue, int startPos, int endPos) { @@ -43,8 +43,7 @@ public Literal(@Nullable String originalValue, int startPos, int endPos) { } - @Nullable - public final String getOriginalValue() { + public final @Nullable String getOriginalValue() { return this.originalValue; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java index e8d15e5b0532..499419c2831f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/MethodReference.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,8 +23,11 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; @@ -41,12 +44,25 @@ import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.ReflectiveMethodExecutor; import org.springframework.expression.spel.support.ReflectiveMethodResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; /** - * Expression language AST node that represents a method reference. + * Expression language AST node that represents a method reference (i.e., a + * method invocation other than a simple property reference). + * + *

    Null-safe Invocation

    + * + *

    Null-safe invocation is supported via the {@code '?.'} operator. For example, + * {@code 'counter?.incrementBy(1)'} will evaluate to {@code null} if {@code counter} + * is {@code null} and will otherwise evaluate to the value returned from the + * invocation of {@code counter.incrementBy(1)}. As of Spring Framework 7.0, + * null-safe invocation also applies when invoking a method on an {@link Optional} + * target. For example, if {@code counter} is of type {@code Optional}, + * the expression {@code 'counter?.incrementBy(1)'} will evaluate to {@code null} + * if {@code counter} is {@code null} or {@link Optional#isEmpty() empty} and will + * otherwise evaluate the value returned from the invocation of + * {@code counter.get().incrementBy(1)}. * * @author Andy Clement * @author Juergen Hoeller @@ -59,11 +75,9 @@ public class MethodReference extends SpelNodeImpl { private final String name; - @Nullable - private Character originalPrimitiveExitTypeDescriptor; + private @Nullable Character originalPrimitiveExitTypeDescriptor; - @Nullable - private volatile CachedMethodExecutor cachedExecutor; + private volatile @Nullable CachedMethodExecutor cachedExecutor; public MethodReference(boolean nullSafe, String methodName, int startPos, int endPos, SpelNodeImpl... arguments) { @@ -91,9 +105,11 @@ public final String getName() { @Override protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { - Object[] arguments = getArguments(state); + @Nullable Object[] arguments = getArguments(state); if (state.getActiveContextObject().getValue() == null) { - throwIfNotNullSafe(getArgumentTypes(arguments)); + if (!isNullSafe()) { + throw nullTargetException(getArgumentTypes(arguments)); + } return ValueRef.NullValueRef.INSTANCE; } return new MethodValueRef(state, arguments); @@ -102,27 +118,45 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException @Override public TypedValue getValueInternal(ExpressionState state) throws EvaluationException { EvaluationContext evaluationContext = state.getEvaluationContext(); - Object value = state.getActiveContextObject().getValue(); - TypeDescriptor targetType = state.getActiveContextObject().getTypeDescriptor(); - Object[] arguments = getArguments(state); - TypedValue result = getValueInternal(evaluationContext, value, targetType, arguments); + TypedValue contextObject = state.getActiveContextObject(); + Object target = contextObject.getValue(); + TypeDescriptor targetType = contextObject.getTypeDescriptor(); + @Nullable Object[] arguments = getArguments(state); + TypedValue result = getValueInternal(evaluationContext, target, targetType, arguments); updateExitTypeDescriptor(); return result; } - private TypedValue getValueInternal(EvaluationContext evaluationContext, - @Nullable Object value, @Nullable TypeDescriptor targetType, Object[] arguments) { + private TypedValue getValueInternal(EvaluationContext evaluationContext, @Nullable Object target, + @Nullable TypeDescriptor targetType, @Nullable Object[] arguments) { List argumentTypes = getArgumentTypes(arguments); - if (value == null) { - throwIfNotNullSafe(argumentTypes); - return TypedValue.NULL; + Optional fallbackOptionalTarget = null; + boolean isEmptyOptional = false; + + if (isNullSafe()) { + if (target == null) { + return TypedValue.NULL; + } + if (target instanceof Optional optional) { + if (optional.isPresent()) { + target = optional.get(); + fallbackOptionalTarget = optional; + } + else { + isEmptyOptional = true; + } + } + } + + if (target == null) { + throw nullTargetException(argumentTypes); } - MethodExecutor executorToUse = getCachedExecutor(evaluationContext, value, targetType, argumentTypes); + MethodExecutor executorToUse = getCachedExecutor(evaluationContext, target, targetType, argumentTypes); if (executorToUse != null) { try { - return executorToUse.execute(evaluationContext, value, arguments); + return executorToUse.execute(evaluationContext, target, arguments); } catch (AccessException ex) { // Two reasons this can occur: @@ -136,40 +170,73 @@ private TypedValue getValueInternal(EvaluationContext evaluationContext, // To determine the situation, the AccessException will contain a cause. // If the cause is an InvocationTargetException, a user exception was // thrown inside the method. Otherwise the method could not be invoked. - throwSimpleExceptionIfPossible(value, ex); + throwSimpleExceptionIfPossible(target, ex); // At this point we know it wasn't a user problem so worth a retry if a // better candidate can be found. this.cachedExecutor = null; + executorToUse = null; + } + } + + // Either there was no cached executor, or it no longer exists. + + // First, attempt to find the method on the target object. + Object targetToUse = target; + MethodExecutorSearchResult searchResult = findMethodExecutor(argumentTypes, target, evaluationContext); + if (searchResult.methodExecutor != null) { + executorToUse = searchResult.methodExecutor; + } + // Second, attempt to find the method on the original Optional instance. + else if (fallbackOptionalTarget != null) { + searchResult = findMethodExecutor(argumentTypes, fallbackOptionalTarget, evaluationContext); + if (searchResult.methodExecutor != null) { + executorToUse = searchResult.methodExecutor; + targetToUse = fallbackOptionalTarget; + } + } + // If we got this far, that means we failed to find an executor for both the + // target and the fallback target. So, we return NULL if the original target + // is a null-safe empty Optional. + else if (isEmptyOptional) { + return TypedValue.NULL; + } + + if (executorToUse == null) { + String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes); + String className = FormatHelper.formatClassNameForMessage( + target instanceof Class clazz ? clazz : target.getClass()); + if (searchResult.accessException != null) { + throw new SpelEvaluationException( + getStartPosition(), searchResult.accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className); + } + else { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_NOT_FOUND, method, className); } } - // either there was no accessor or it no longer existed - executorToUse = findAccessorForMethod(argumentTypes, value, evaluationContext); this.cachedExecutor = new CachedMethodExecutor( - executorToUse, (value instanceof Class clazz ? clazz : null), targetType, argumentTypes); + executorToUse, (targetToUse instanceof Class clazz ? clazz : null), targetType, argumentTypes); try { - return executorToUse.execute(evaluationContext, value, arguments); + return executorToUse.execute(evaluationContext, targetToUse, arguments); } catch (AccessException ex) { - // Same unwrapping exception handling as above in above catch block - throwSimpleExceptionIfPossible(value, ex); + // Same unwrapping exception handling as in above catch block + throwSimpleExceptionIfPossible(targetToUse, ex); throw new SpelEvaluationException(getStartPosition(), ex, SpelMessage.EXCEPTION_DURING_METHOD_INVOCATION, this.name, - value.getClass().getName(), ex.getMessage()); + targetToUse.getClass().getName(), ex.getMessage()); } } - private void throwIfNotNullSafe(List argumentTypes) { - if (!this.nullSafe) { - throw new SpelEvaluationException(getStartPosition(), - SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED, - FormatHelper.formatMethodForMessage(this.name, argumentTypes)); - } + private SpelEvaluationException nullTargetException(List argumentTypes) { + return new SpelEvaluationException(getStartPosition(), + SpelMessage.METHOD_CALL_ON_NULL_OBJECT_NOT_ALLOWED, + FormatHelper.formatMethodForMessage(this.name, argumentTypes)); } - private Object[] getArguments(ExpressionState state) { - Object[] arguments = new Object[getChildCount()]; + private @Nullable Object[] getArguments(ExpressionState state) { + @Nullable Object[] arguments = new Object[getChildCount()]; for (int i = 0; i < arguments.length; i++) { // Make the root object the active context again for evaluating the parameter expressions try { @@ -183,17 +250,16 @@ private Object[] getArguments(ExpressionState state) { return arguments; } - private List getArgumentTypes(Object... arguments) { - List descriptors = new ArrayList<>(arguments.length); + private List getArgumentTypes(@Nullable Object... arguments) { + List<@Nullable TypeDescriptor> descriptors = new ArrayList<>(arguments.length); for (Object argument : arguments) { descriptors.add(TypeDescriptor.forObject(argument)); } return Collections.unmodifiableList(descriptors); } - @Nullable - private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object value, - @Nullable TypeDescriptor target, List argumentTypes) { + private @Nullable MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Object target, + @Nullable TypeDescriptor targetType, List argumentTypes) { List methodResolvers = evaluationContext.getMethodResolvers(); if (methodResolvers.size() != 1 || !(methodResolvers.get(0) instanceof ReflectiveMethodResolver)) { @@ -202,23 +268,23 @@ private MethodExecutor getCachedExecutor(EvaluationContext evaluationContext, Ob } CachedMethodExecutor executorToCheck = this.cachedExecutor; - if (executorToCheck != null && executorToCheck.isSuitable(value, target, argumentTypes)) { + if (executorToCheck != null && executorToCheck.isSuitable(target, targetType, argumentTypes)) { return executorToCheck.get(); } this.cachedExecutor = null; return null; } - private MethodExecutor findAccessorForMethod(List argumentTypes, Object targetObject, + private MethodExecutorSearchResult findMethodExecutor(List argumentTypes, Object target, EvaluationContext evaluationContext) throws SpelEvaluationException { AccessException accessException = null; for (MethodResolver methodResolver : evaluationContext.getMethodResolvers()) { try { MethodExecutor methodExecutor = methodResolver.resolve( - evaluationContext, targetObject, this.name, argumentTypes); + evaluationContext, target, this.name, argumentTypes); if (methodExecutor != null) { - return methodExecutor; + return new MethodExecutorSearchResult(methodExecutor, null); } } catch (AccessException ex) { @@ -227,23 +293,14 @@ private MethodExecutor findAccessorForMethod(List argumentTypes, } } - String method = FormatHelper.formatMethodForMessage(this.name, argumentTypes); - String className = FormatHelper.formatClassNameForMessage( - targetObject instanceof Class clazz ? clazz : targetObject.getClass()); - if (accessException != null) { - throw new SpelEvaluationException( - getStartPosition(), accessException, SpelMessage.PROBLEM_LOCATING_METHOD, method, className); - } - else { - throw new SpelEvaluationException(getStartPosition(), SpelMessage.METHOD_NOT_FOUND, method, className); - } + return new MethodExecutorSearchResult(null, accessException); } /** * Decode the AccessException, throwing a lightweight evaluation exception or, * if the cause was a RuntimeException, throw the RuntimeException directly. */ - private void throwSimpleExceptionIfPossible(Object value, AccessException ex) { + private void throwSimpleExceptionIfPossible(Object target, AccessException ex) { if (ex.getCause() instanceof InvocationTargetException cause) { Throwable rootCause = cause.getCause(); if (rootCause instanceof RuntimeException runtimeException) { @@ -251,7 +308,7 @@ private void throwSimpleExceptionIfPossible(Object value, AccessException ex) { } throw new ExpressionInvocationTargetException(getStartPosition(), "A problem occurred when trying to execute method '" + this.name + - "' on object of type [" + value.getClass().getName() + "]", rootCause); + "' on object of type [" + target.getClass().getName() + "]", rootCause); } } @@ -260,7 +317,7 @@ private void updateExitTypeDescriptor() { if (executorToCheck != null && executorToCheck.get() instanceof ReflectiveMethodExecutor reflectiveMethodExecutor) { Method method = reflectiveMethodExecutor.getMethod(); String descriptor = CodeFlow.toDescriptor(method.getReturnType()); - if (this.nullSafe && CodeFlow.isPrimitive(descriptor) && (descriptor.charAt(0) != 'V')) { + if (isNullSafe() && CodeFlow.isPrimitive(descriptor) && (descriptor.charAt(0) != 'V')) { this.originalPrimitiveExitTypeDescriptor = descriptor.charAt(0); this.exitTypeDescriptor = CodeFlow.toBoxedDescriptor(descriptor); } @@ -326,7 +383,7 @@ public void generateCode(MethodVisitor mv, CodeFlow cf) { } Label skipIfNull = null; - if (this.nullSafe && (descriptor != null || !isStatic)) { + if (isNullSafe() && (descriptor != null || !isStatic)) { skipIfNull = new Label(); Label continueLabel = new Label(); mv.visitInsn(DUP); @@ -378,17 +435,15 @@ private class MethodValueRef implements ValueRef { private final EvaluationContext evaluationContext; - @Nullable - private final Object value; + private final @Nullable Object target; - @Nullable - private final TypeDescriptor targetType; + private final @Nullable TypeDescriptor targetType; - private final Object[] arguments; + private final @Nullable Object[] arguments; - public MethodValueRef(ExpressionState state, Object[] arguments) { + public MethodValueRef(ExpressionState state, @Nullable Object[] arguments) { this.evaluationContext = state.getEvaluationContext(); - this.value = state.getActiveContextObject().getValue(); + this.target = state.getActiveContextObject().getValue(); this.targetType = state.getActiveContextObject().getTypeDescriptor(); this.arguments = arguments; } @@ -396,7 +451,7 @@ public MethodValueRef(ExpressionState state, Object[] arguments) { @Override public TypedValue getValue() { TypedValue result = MethodReference.this.getValueInternal( - this.evaluationContext, this.value, this.targetType, this.arguments); + this.evaluationContext, this.target, this.targetType, this.arguments); updateExitTypeDescriptor(); return result; } @@ -413,34 +468,19 @@ public boolean isWritable() { } - private static class CachedMethodExecutor { - - private final MethodExecutor methodExecutor; - - @Nullable - private final Class staticClass; - - @Nullable - private final TypeDescriptor target; - - private final List argumentTypes; - - public CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class staticClass, - @Nullable TypeDescriptor target, List argumentTypes) { + private record MethodExecutorSearchResult(@Nullable MethodExecutor methodExecutor, @Nullable AccessException accessException) { + } - this.methodExecutor = methodExecutor; - this.staticClass = staticClass; - this.target = target; - this.argumentTypes = argumentTypes; - } + private record CachedMethodExecutor(MethodExecutor methodExecutor, @Nullable Class staticClass, + @Nullable TypeDescriptor targetType, List argumentTypes) { - public boolean isSuitable(Object value, @Nullable TypeDescriptor target, List argumentTypes) { - return ((this.staticClass == null || this.staticClass == value) && - ObjectUtils.nullSafeEquals(this.target, target) && this.argumentTypes.equals(argumentTypes)); + public boolean isSuitable(Object target, @Nullable TypeDescriptor targetType, List argumentTypes) { + return ((this.staticClass == null || this.staticClass == target) && + ObjectUtils.nullSafeEquals(this.targetType, targetType) && this.argumentTypes.equals(argumentTypes)); } public boolean hasProxyTarget() { - return (this.target != null && Proxy.isProxyClass(this.target.getType())); + return (this.targetType != null && Proxy.isProxyClass(this.targetType.getType())); } public MethodExecutor get() { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java index 99c5694095f1..e91050a3d0a2 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpAnd.java @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; @@ -26,7 +28,6 @@ import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.BooleanTypedValue; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; /** * Represents the boolean AND operation. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java index 4caf753c85a5..4383b68b49b0 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpOr.java @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationException; @@ -25,7 +27,6 @@ import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.BooleanTypedValue; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; /** * Represents the boolean OR operation. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java index 910031017738..485aacb33aa7 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OpPlus.java @@ -19,6 +19,8 @@ import java.math.BigDecimal; import java.math.BigInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; @@ -29,7 +31,6 @@ import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.NumberUtils; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java index a425fd4f4f0f..a81e40609c08 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Operator.java @@ -19,11 +19,12 @@ import java.math.BigDecimal; import java.math.BigInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.spel.CodeFlow; -import org.springframework.lang.Nullable; import org.springframework.util.NumberUtils; import org.springframework.util.ObjectUtils; @@ -47,11 +48,9 @@ public abstract class Operator extends SpelNodeImpl { // whose accessors seem to only be returning 'Object' - the actual descriptors may // indicate 'int') - @Nullable - protected String leftActualDescriptor; + protected @Nullable String leftActualDescriptor; - @Nullable - protected String rightActualDescriptor; + protected @Nullable String rightActualDescriptor; public Operator(String payload, int startPos, int endPos, SpelNodeImpl... operands) { @@ -352,7 +351,7 @@ private DescriptorComparison(boolean areNumbers, boolean areCompatible, char com * @param rightActualDescriptor the dynamic/runtime right object descriptor * @return a DescriptorComparison object indicating the type of compatibility, if any */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public static DescriptorComparison checkNumericCompatibility( @Nullable String leftDeclaredDescriptor, @Nullable String rightDeclaredDescriptor, @Nullable String leftActualDescriptor, @Nullable String rightActualDescriptor) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java index 36dfc4c4366b..d106ce507ce5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/OperatorInstanceof.java @@ -16,6 +16,8 @@ package org.springframework.expression.spel.ast; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.asm.Type; import org.springframework.expression.EvaluationException; @@ -25,7 +27,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.BooleanTypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ */ public class OperatorInstanceof extends Operator { - @Nullable - private Class type; + private @Nullable Class type; public OperatorInstanceof(int startPos, int endPos, SpelNodeImpl... operands) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java index 56d0b2f34d2d..8ba69a498748 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Projection.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,23 +21,38 @@ import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; /** - * Represents projection, where a given operation is performed on all elements in some - * input sequence, returning a new sequence of the same size. + * Represents projection, where a given operation is performed on all elements in + * some input sequence, returning a new sequence of the same size. * *

    For example: {1,2,3,4,5,6,7,8,9,10}.![#isEven(#this)] evaluates * to {@code [n, y, n, y, n, y, n, y, n, y]}. * + *

    Null-safe Projection

    + * + *

    Null-safe projection is supported via the {@code '?.!'} operator. For example, + * {@code 'names?.![#this.length]'} will evaluate to {@code null} if {@code names} + * is {@code null} and will otherwise evaluate to a sequence containing the lengths + * of the names. As of Spring Framework 7.0, null-safe projection also applies when + * performing projection on an {@link Optional} target. For example, if {@code names} + * is of type {@code Optional>}, the expression + * {@code 'names?.![#this.length]'} will evaluate to {@code null} if {@code names} + * is {@code null} or {@link Optional#isEmpty() empty} and will otherwise evaluate + * to a sequence containing the lengths of the names, effectively + * {@code names.get().stream().map(String::length).toList()}. + * * @author Andy Clement * @author Mark Fisher * @author Juergen Hoeller @@ -71,8 +86,24 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep @Override protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { - TypedValue op = state.getActiveContextObject(); - Object operand = op.getValue(); + TypedValue contextObject = state.getActiveContextObject(); + Object operand = contextObject.getValue(); + + if (isNullSafe()) { + if (operand == null) { + return ValueRef.NullValueRef.INSTANCE; + } + if (operand instanceof Optional optional) { + if (optional.isEmpty()) { + return ValueRef.NullValueRef.INSTANCE; + } + operand = optional.get(); + } + } + + if (operand == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, "null"); + } // When the input is a map, we push a Map.Entry on the stack before calling // the specified operation. Map.Entry has two properties 'key' and 'value' @@ -129,13 +160,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException return new ValueRef.TypedValueHolderValueRef(new TypedValue(result),this); } - if (operand == null) { - if (this.nullSafe) { - return ValueRef.NullValueRef.INSTANCE; - } - throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, "null"); - } - throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROJECTION_NOT_SUPPORTED_ON_TYPE, operand.getClass().getName()); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java index c40dc8a61dcf..078be1c45866 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/PropertyOrFieldReference.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,8 +21,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.Label; import org.springframework.asm.MethodVisitor; import org.springframework.core.convert.TypeDescriptor; @@ -37,12 +40,23 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.ReflectivePropertyAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; /** - * Represents a simple property or field reference. + * Represents a simple public property or field reference. + * + *

    Null-safe Navigation

    + * + *

    Null-safe navigation is supported via the {@code '?.'} operator. For example, + * {@code 'user?.name'} will evaluate to {@code null} if {@code user} is {@code null} + * and will otherwise evaluate to the name of the user. As of Spring Framework 7.0, + * null-safe navigation also applies when accessing a property or field on an + * {@link Optional} target. For example, if {@code user} is of type + * {@code Optional}, the expression {@code 'user?.name'} will evaluate to + * {@code null} if {@code user} is {@code null} or {@link Optional#isEmpty() empty} + * and will otherwise evaluate to the name of the user, effectively + * {@code user.get().getName()} or {@code user.get().name}. * * @author Andy Clement * @author Juergen Hoeller @@ -56,14 +70,11 @@ public class PropertyOrFieldReference extends SpelNodeImpl { private final String name; - @Nullable - private String originalPrimitiveExitTypeDescriptor; + private @Nullable String originalPrimitiveExitTypeDescriptor; - @Nullable - private volatile PropertyAccessor cachedReadAccessor; + private volatile @Nullable PropertyAccessor cachedReadAccessor; - @Nullable - private volatile PropertyAccessor cachedWriteAccessor; + private volatile @Nullable PropertyAccessor cachedWriteAccessor; public PropertyOrFieldReference(boolean nullSafe, String propertyOrFieldName, int startPos, int endPos) { @@ -181,16 +192,31 @@ public String toStringAST() { private TypedValue readProperty(TypedValue contextObject, EvaluationContext evalContext, String name) throws EvaluationException { - Object targetObject = contextObject.getValue(); - if (targetObject == null && isNullSafe()) { - return TypedValue.NULL; + final Object originalTarget = contextObject.getValue(); + Object target = originalTarget; + Optional fallbackOptionalTarget = null; + boolean isEmptyOptional = false; + + if (isNullSafe()) { + if (target == null) { + return TypedValue.NULL; + } + if (target instanceof Optional optional) { + if (optional.isPresent()) { + target = optional.get(); + fallbackOptionalTarget = optional; + } + else { + isEmptyOptional = true; + } + } } PropertyAccessor accessorToUse = this.cachedReadAccessor; if (accessorToUse != null) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) { try { - return accessorToUse.read(evalContext, targetObject, name); + return accessorToUse.read(evalContext, target, name); } catch (Exception ex) { // This is OK - it may have gone stale due to a class change, @@ -201,19 +227,29 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval } List accessorsToTry = - AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); + AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors()); // Go through the accessors that may be able to resolve it. If they are a cacheable accessor then // get the accessor and use it. If they are not cacheable but report they can read the property - // then ask them to read it + // then ask them to read it. try { for (PropertyAccessor accessor : accessorsToTry) { - if (accessor.canRead(evalContext, targetObject, name)) { + // First, attempt to find the property on the target object. + if (accessor.canRead(evalContext, target, name)) { if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) { accessor = reflectivePropertyAccessor.createOptimalAccessor( - evalContext, targetObject, name); + evalContext, target, name); } this.cachedReadAccessor = accessor; - return accessor.read(evalContext, targetObject, name); + return accessor.read(evalContext, target, name); + } + // Second, attempt to find the property on the original Optional instance. + else if (fallbackOptionalTarget != null && accessor.canRead(evalContext, fallbackOptionalTarget, name)) { + if (accessor instanceof ReflectivePropertyAccessor reflectivePropertyAccessor) { + accessor = reflectivePropertyAccessor.createOptimalAccessor( + evalContext, fallbackOptionalTarget, name); + } + this.cachedReadAccessor = accessor; + return accessor.read(evalContext, fallbackOptionalTarget, name); } } } @@ -221,12 +257,19 @@ private TypedValue readProperty(TypedValue contextObject, EvaluationContext eval throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_DURING_PROPERTY_READ, name, ex.getMessage()); } - if (contextObject.getValue() == null) { + // If we got this far, that means we failed to find an accessor for both the + // target and the fallback target. So, we return NULL if the original target + // is a null-safe empty Optional. + if (isEmptyOptional) { + return TypedValue.NULL; + } + + if (originalTarget == null) { throw new SpelEvaluationException(SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL, name); } else { throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE, name, - FormatHelper.formatClassNameForMessage(getObjectClass(contextObject.getValue()))); + FormatHelper.formatClassNameForMessage(getObjectClass(originalTarget))); } } @@ -234,8 +277,8 @@ private void writeProperty( TypedValue contextObject, EvaluationContext evalContext, String name, @Nullable Object newValue) throws EvaluationException { - Object targetObject = contextObject.getValue(); - if (targetObject == null) { + Object target = contextObject.getValue(); + if (target == null) { if (isNullSafe()) { return; } @@ -247,7 +290,7 @@ private void writeProperty( if (accessorToUse != null) { if (evalContext.getPropertyAccessors().contains(accessorToUse)) { try { - accessorToUse.write(evalContext, targetObject, name, newValue); + accessorToUse.write(evalContext, target, name, newValue); return; } catch (Exception ex) { @@ -259,12 +302,12 @@ private void writeProperty( } List accessorsToTry = - AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); + AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors()); try { for (PropertyAccessor accessor : accessorsToTry) { - if (accessor.canWrite(evalContext, targetObject, name)) { + if (accessor.canWrite(evalContext, target, name)) { this.cachedWriteAccessor = accessor; - accessor.write(evalContext, targetObject, name, newValue); + accessor.write(evalContext, target, name, newValue); return; } } @@ -275,19 +318,19 @@ private void writeProperty( } throw new SpelEvaluationException(getStartPosition(), SpelMessage.PROPERTY_OR_FIELD_NOT_WRITABLE, name, - FormatHelper.formatClassNameForMessage(getObjectClass(targetObject))); + FormatHelper.formatClassNameForMessage(getObjectClass(target))); } public boolean isWritableProperty(String name, TypedValue contextObject, EvaluationContext evalContext) throws EvaluationException { - Object targetObject = contextObject.getValue(); - if (targetObject != null) { + Object target = contextObject.getValue(); + if (target != null) { List accessorsToTry = - AccessorUtils.getAccessorsToTry(targetObject, evalContext.getPropertyAccessors()); + AccessorUtils.getAccessorsToTry(target, evalContext.getPropertyAccessors()); for (PropertyAccessor accessor : accessorsToTry) { try { - if (accessor.canWrite(evalContext, targetObject, name)) { + if (accessor.canWrite(evalContext, target, name)) { return true; } } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java index d66a4c39ce83..1acd99787c78 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/QualifiedIdentifier.java @@ -16,10 +16,11 @@ package org.springframework.expression.spel.ast; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.ExpressionState; -import org.springframework.lang.Nullable; /** * Represents a dot separated sequence of strings that indicate a package qualified type @@ -32,8 +33,7 @@ */ public class QualifiedIdentifier extends SpelNodeImpl { - @Nullable - private TypedValue value; + private @Nullable TypedValue value; public QualifiedIdentifier(int startPos, int endPos, SpelNodeImpl... operands) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java index d7a76ad40d23..abe2e36486b9 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/Selection.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,6 +22,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; @@ -35,7 +36,7 @@ import org.springframework.util.ObjectUtils; /** - * Represents selection over a map or collection. + * Represents selection over a {@link Map}, {@link Iterable}, or array. * *

    For example, {1,2,3,4,5,6,7,8,9,10}.?[#isEven(#this)] evaluates * to {@code [2, 4, 6, 8, 10]}. @@ -43,6 +44,19 @@ *

    Basically a subset of the input data is returned based on the evaluation of * the expression supplied as selection criteria. * + *

    Null-safe Selection

    + * + *

    Null-safe selection is supported via the {@code '?.?'} operator. For example, + * {@code 'names?.?[#this.length > 5]'} will evaluate to {@code null} if {@code names} + * is {@code null} and will otherwise evaluate to a sequence containing the names + * whose length is greater than 5. As of Spring Framework 7.0, null-safe selection + * also applies when performing selection on an {@link Optional} target. For example, + * if {@code names} is of type {@code Optional>}, the expression + * {@code 'names?.?[#this.length > 5]'} will evaluate to {@code null} if {@code names} + * is {@code null} or {@link Optional#isEmpty() empty} and will otherwise evaluate + * to a sequence containing the names whose lengths are greater than 5, effectively + * {@code names.get().stream().filter(s -> s.length() > 5).toList()}. + * * @author Andy Clement * @author Mark Fisher * @author Sam Brannen @@ -94,8 +108,25 @@ public TypedValue getValueInternal(ExpressionState state) throws EvaluationExcep @Override protected ValueRef getValueRef(ExpressionState state) throws EvaluationException { - TypedValue op = state.getActiveContextObject(); - Object operand = op.getValue(); + TypedValue contextObject = state.getActiveContextObject(); + Object operand = contextObject.getValue(); + + if (isNullSafe()) { + if (operand == null) { + return ValueRef.NullValueRef.INSTANCE; + } + if (operand instanceof Optional optional) { + if (optional.isEmpty()) { + return ValueRef.NullValueRef.INSTANCE; + } + operand = optional.get(); + } + } + + if (operand == null) { + throw new SpelEvaluationException(getStartPosition(), SpelMessage.INVALID_TYPE_FOR_SELECTION, "null"); + } + SpelNodeImpl selectionCriteria = this.children[0]; if (operand instanceof Map mapdata) { @@ -151,9 +182,9 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException try { state.pushActiveContextObject(new TypedValue(element)); state.enterScope(); - Object val = selectionCriteria.getValueInternal(state).getValue(); - if (val instanceof Boolean b) { - if (b) { + Object criteria = selectionCriteria.getValueInternal(state).getValue(); + if (criteria instanceof Boolean match) { + if (match) { if (this.variant == FIRST) { return new ValueRef.TypedValueHolderValueRef(new TypedValue(element), this); } @@ -184,7 +215,7 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException } Class elementType = null; - TypeDescriptor typeDesc = op.getTypeDescriptor(); + TypeDescriptor typeDesc = contextObject.getTypeDescriptor(); if (typeDesc != null) { TypeDescriptor elementTypeDesc = typeDesc.getElementTypeDescriptor(); if (elementTypeDesc != null) { @@ -198,13 +229,6 @@ protected ValueRef getValueRef(ExpressionState state) throws EvaluationException return new ValueRef.TypedValueHolderValueRef(new TypedValue(resultArray), this); } - if (operand == null) { - if (this.nullSafe) { - return ValueRef.NullValueRef.INSTANCE; - } - throw new SpelEvaluationException(getStartPosition(), SpelMessage.INVALID_TYPE_FOR_SELECTION, "null"); - } - throw new SpelEvaluationException(getStartPosition(), SpelMessage.INVALID_TYPE_FOR_SELECTION, operand.getClass().getName()); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java index b5ed9d0e2cc9..6543cf5527d3 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/SpelNodeImpl.java @@ -20,6 +20,8 @@ import java.lang.reflect.Member; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.asm.Opcodes; import org.springframework.asm.Type; @@ -31,7 +33,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.SpelNode; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -57,8 +58,7 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes { protected SpelNodeImpl[] children = SpelNodeImpl.NO_CHILDREN; - @Nullable - private SpelNodeImpl parent; + private @Nullable SpelNodeImpl parent; /** * Indicates the type descriptor for the result of this expression node. @@ -69,11 +69,10 @@ public abstract class SpelNodeImpl implements SpelNode, Opcodes { * It does not include the trailing semicolon (for non array reference types). * Some examples: Ljava/lang/String, I, [I */ - @Nullable - protected volatile String exitTypeDescriptor; + protected volatile @Nullable String exitTypeDescriptor; - public SpelNodeImpl(int startPos, int endPos, @Nullable SpelNodeImpl... operands) { + public SpelNodeImpl(int startPos, int endPos, SpelNodeImpl @Nullable ... operands) { this.startPos = startPos; this.endPos = endPos; if (!ObjectUtils.isEmpty(operands)) { @@ -111,8 +110,7 @@ protected boolean nextChildIs(Class... classes) { } @Override - @Nullable - public final Object getValue(ExpressionState expressionState) throws EvaluationException { + public final @Nullable Object getValue(ExpressionState expressionState) throws EvaluationException { return getValueInternal(expressionState).getValue(); } @@ -165,8 +163,7 @@ public int getChildCount() { } @Override - @Nullable - public Class getObjectClass(@Nullable Object obj) { + public @Nullable Class getObjectClass(@Nullable Object obj) { if (obj == null) { return null; } @@ -193,13 +190,11 @@ public boolean isNullSafe() { return false; } - @Nullable - public String getExitDescriptor() { + public @Nullable String getExitDescriptor() { return this.exitTypeDescriptor; } - @Nullable - protected final T getValue(ExpressionState state, Class desiredReturnType) throws EvaluationException { + protected final @Nullable T getValue(ExpressionState state, Class desiredReturnType) throws EvaluationException { return ExpressionUtils.convertTypedValue(state.getEvaluationContext(), getValueInternal(state), desiredReturnType); } @@ -302,8 +297,7 @@ protected static void generateCodeForArguments( } } - @Nullable - private static Class loadClassForExitDescriptor(@Nullable String exitDescriptor, ClassLoader classLoader) { + private static @Nullable Class loadClassForExitDescriptor(@Nullable String exitDescriptor, ClassLoader classLoader) { if (!StringUtils.hasText(exitDescriptor)) { return null; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java index 360e7598fa4e..21cd5c469445 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/TypeReference.java @@ -19,13 +19,14 @@ import java.lang.reflect.Array; import java.util.Locale; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.asm.Type; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,8 +40,7 @@ public class TypeReference extends SpelNodeImpl { private final int dimensions; - @Nullable - private transient Class type; + private transient @Nullable Class type; public TypeReference(int startPos, int endPos, SpelNodeImpl qualifiedId) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java index dd199ccb54e7..c38c0c9b0eac 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/ValueRef.java @@ -16,10 +16,11 @@ package org.springframework.expression.spel.ast; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.TypedValue; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; /** * Represents a reference to a value. With a reference it is possible to get or set the diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java index 31cf1f699d0d..7bb013b9c855 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/VariableReference.java @@ -19,6 +19,8 @@ import java.lang.reflect.Modifier; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; @@ -26,7 +28,6 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.ExpressionState; import org.springframework.expression.spel.SpelEvaluationException; -import org.springframework.lang.Nullable; /** * Represents a variable reference — for example, {@code #root}, {@code #this}, diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java index d6c0b5ec6d98..cba46a2d7d36 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/package-info.java @@ -1,9 +1,7 @@ /** * SpEL's abstract syntax tree. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.expression.spel.ast; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java index 81dc5f52c6da..27ee1302254a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/package-info.java @@ -1,9 +1,7 @@ /** * SpEL's central implementation package. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.expression.spel; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java index c6bbfbfc29c3..aa10ab879244 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java @@ -26,6 +26,8 @@ import java.util.concurrent.ConcurrentMap; import java.util.regex.Pattern; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateAwareExpressionParser; @@ -78,7 +80,6 @@ import org.springframework.expression.spel.ast.TypeReference; import org.springframework.expression.spel.ast.VariableReference; import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -165,9 +166,8 @@ private void checkExpressionLength(String string) { // | (DEFAULT^ logicalOrExpression) // | (QMARK^ expression COLON! expression) // | (ELVIS^ expression))?; - @Nullable - @SuppressWarnings("NullAway") - private SpelNodeImpl eatExpression() { + @SuppressWarnings("NullAway") // Not null assertion performed in SpelNodeImpl constructor + private @Nullable SpelNodeImpl eatExpression() { SpelNodeImpl expr = eatLogicalOrExpression(); Token t = peekToken(); if (t != null) { @@ -205,8 +205,7 @@ private SpelNodeImpl eatExpression() { } //logicalOrExpression : logicalAndExpression (OR^ logicalAndExpression)*; - @Nullable - private SpelNodeImpl eatLogicalOrExpression() { + private @Nullable SpelNodeImpl eatLogicalOrExpression() { SpelNodeImpl expr = eatLogicalAndExpression(); while (peekIdentifierToken("or") || peekToken(TokenKind.SYMBOLIC_OR)) { Token t = takeToken(); //consume OR @@ -218,8 +217,7 @@ private SpelNodeImpl eatLogicalOrExpression() { } // logicalAndExpression : relationalExpression (AND^ relationalExpression)*; - @Nullable - private SpelNodeImpl eatLogicalAndExpression() { + private @Nullable SpelNodeImpl eatLogicalAndExpression() { SpelNodeImpl expr = eatRelationalExpression(); while (peekIdentifierToken("and") || peekToken(TokenKind.SYMBOLIC_AND)) { Token t = takeToken(); // consume 'AND' @@ -231,8 +229,7 @@ private SpelNodeImpl eatLogicalAndExpression() { } // relationalExpression : sumExpression (relationalOperator^ sumExpression)?; - @Nullable - private SpelNodeImpl eatRelationalExpression() { + private @Nullable SpelNodeImpl eatRelationalExpression() { SpelNodeImpl expr = eatSumExpression(); Token relationalOperatorToken = maybeEatRelationalOperator(); if (relationalOperatorToken != null) { @@ -276,9 +273,8 @@ private SpelNodeImpl eatRelationalExpression() { } //sumExpression: productExpression ( (PLUS^ | MINUS^) productExpression)*; - @Nullable - @SuppressWarnings("NullAway") - private SpelNodeImpl eatSumExpression() { + @SuppressWarnings("NullAway") // Not null assertion performed in SpelNodeImpl constructor + private @Nullable SpelNodeImpl eatSumExpression() { SpelNodeImpl expr = eatProductExpression(); while (peekToken(TokenKind.PLUS, TokenKind.MINUS, TokenKind.INC)) { Token t = takeToken(); //consume PLUS or MINUS or INC @@ -295,8 +291,7 @@ else if (t.kind == TokenKind.MINUS) { } // productExpression: powerExpr ((STAR^ | DIV^| MOD^) powerExpr)* ; - @Nullable - private SpelNodeImpl eatProductExpression() { + private @Nullable SpelNodeImpl eatProductExpression() { SpelNodeImpl expr = eatPowerIncDecExpression(); while (peekToken(TokenKind.STAR, TokenKind.DIV, TokenKind.MOD)) { Token t = takeToken(); // consume STAR/DIV/MOD @@ -316,9 +311,8 @@ else if (t.kind == TokenKind.MOD) { } // powerExpr : unaryExpression (POWER^ unaryExpression)? (INC || DEC) ; - @Nullable - @SuppressWarnings("NullAway") - private SpelNodeImpl eatPowerIncDecExpression() { + @SuppressWarnings("NullAway") // Not null assertion performed in SpelNodeImpl constructor + private @Nullable SpelNodeImpl eatPowerIncDecExpression() { SpelNodeImpl expr = eatUnaryExpression(); if (peekToken(TokenKind.POWER)) { Token t = takeToken(); //consume POWER @@ -337,9 +331,8 @@ private SpelNodeImpl eatPowerIncDecExpression() { } // unaryExpression: (PLUS^ | MINUS^ | BANG^ | INC^ | DEC^) unaryExpression | primaryExpression ; - @Nullable - @SuppressWarnings("NullAway") - private SpelNodeImpl eatUnaryExpression() { + @SuppressWarnings("NullAway") // Not null assertion performed in SpelNodeImpl constructor + private @Nullable SpelNodeImpl eatUnaryExpression() { if (peekToken(TokenKind.NOT, TokenKind.PLUS, TokenKind.MINUS)) { Token t = takeToken(); SpelNodeImpl expr = eatUnaryExpression(); @@ -370,8 +363,7 @@ private SpelNodeImpl eatUnaryExpression() { } // primaryExpression : startNode (node)? -> ^(EXPRESSION startNode (node)?); - @Nullable - private SpelNodeImpl eatPrimaryExpression() { + private @Nullable SpelNodeImpl eatPrimaryExpression() { SpelNodeImpl start = eatStartNode(); // always a start node List nodes = null; SpelNodeImpl node = eatNode(); @@ -391,14 +383,12 @@ private SpelNodeImpl eatPrimaryExpression() { } // node : ((DOT dottedNode) | (SAFE_NAVI dottedNode) | nonDottedNode)+; - @Nullable - private SpelNodeImpl eatNode() { + private @Nullable SpelNodeImpl eatNode() { return (peekToken(TokenKind.DOT, TokenKind.SAFE_NAVI) ? eatDottedNode() : eatNonDottedNode()); } // nonDottedNode: indexer; - @Nullable - private SpelNodeImpl eatNonDottedNode() { + private @Nullable SpelNodeImpl eatNonDottedNode() { if (peekToken(TokenKind.LSQUARE)) { if (maybeEatIndexer(false)) { return pop(); @@ -457,8 +447,7 @@ private boolean maybeEatFunctionOrVar() { } // methodArgs : LPAREN! (argument (COMMA! argument)* (COMMA!)?)? RPAREN!; - @Nullable - private SpelNodeImpl[] maybeEatMethodArgs() { + private SpelNodeImpl @Nullable [] maybeEatMethodArgs() { if (!peekToken(TokenKind.LPAREN)) { return null; } @@ -524,8 +513,7 @@ private int positionOf(@Nullable Token t) { // | lastSelection // | indexer // | constructor - @Nullable - private SpelNodeImpl eatStartNode() { + private @Nullable SpelNodeImpl eatStartNode() { if (maybeEatLiteral()) { return pop(); } @@ -917,8 +905,7 @@ private boolean maybeEatParenExpression() { // relationalOperator // : EQUAL | NOT_EQUAL | LESS_THAN | LESS_THAN_OR_EQUAL | GREATER_THAN // | GREATER_THAN_OR_EQUAL | INSTANCEOF | BETWEEN | MATCHES - @Nullable - private Token maybeEatRelationalOperator() { + private @Nullable Token maybeEatRelationalOperator() { Token t = peekToken(); if (t == null) { return null; @@ -1022,16 +1009,14 @@ private Token takeToken() { return this.tokenStream.get(this.tokenStreamPointer++); } - @Nullable - private Token nextToken() { + private @Nullable Token nextToken() { if (this.tokenStreamPointer >= this.tokenStreamLength) { return null; } return this.tokenStream.get(this.tokenStreamPointer++); } - @Nullable - private Token peekToken() { + private @Nullable Token peekToken() { if (this.tokenStreamPointer >= this.tokenStreamLength) { return null; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java index e8802052bd81..1b37c1e887be 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelCompiler.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.asm.ClassWriter; import org.springframework.asm.MethodVisitor; @@ -32,7 +33,6 @@ import org.springframework.expression.spel.CompiledExpression; import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.ast.SpelNodeImpl; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ReflectionUtils; @@ -98,8 +98,7 @@ private SpelCompiler(@Nullable ClassLoader classloader) { * @return an instance of the class implementing the compiled expression, * or {@code null} if compilation is not possible */ - @Nullable - public CompiledExpression compile(SpelNodeImpl expression) { + public @Nullable CompiledExpression compile(SpelNodeImpl expression) { if (expression.isCompilable()) { if (logger.isDebugEnabled()) { logger.debug("SpEL: compiling " + expression.toStringAST()); @@ -133,14 +132,13 @@ private String getNextSuffix() { * @return the expression call, or {@code null} if the decision was to opt out of * compilation during code generation */ - @Nullable - private Class createExpressionClass(SpelNodeImpl expressionToCompile) { + private @Nullable Class createExpressionClass(SpelNodeImpl expressionToCompile) { // Create class outline: // org.springframework.expression.spel.generated.CompiledExpression##### extends org.springframework.expression.spel.CompiledExpression String className = "org/springframework/expression/spel/generated/CompiledExpression" + getNextSuffix(); String evaluationContextClass = "org/springframework/expression/EvaluationContext"; ClassWriter cw = new ExpressionClassWriter(); - cw.visit(V1_8, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null); + cw.visit(V17, ACC_PUBLIC, className, null, "org/springframework/expression/spel/CompiledExpression", null); // Create default constructor MethodVisitor mv = cw.visitMethod(ACC_PUBLIC, "", "()V", null, null); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java index f4ea32211f17..a101d38453f9 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java @@ -18,6 +18,8 @@ import java.util.concurrent.atomic.AtomicInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.EvaluationException; @@ -33,7 +35,6 @@ import org.springframework.expression.spel.SpelParserConfiguration; import org.springframework.expression.spel.ast.SpelNodeImpl; import org.springframework.expression.spel.support.StandardEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,12 +64,10 @@ public class SpelExpression implements Expression { private final SpelParserConfiguration configuration; // The default context is used if no override is supplied by the user - @Nullable - private EvaluationContext evaluationContext; + private @Nullable EvaluationContext evaluationContext; // Holds the compiled form of the expression (if it has been compiled) - @Nullable - private volatile CompiledExpression compiledAst; + private volatile @Nullable CompiledExpression compiledAst; // Count of many times as the expression been interpreted - can trigger compilation // when certain limit reached @@ -117,8 +116,7 @@ public String getExpressionString() { } @Override - @Nullable - public Object getValue() throws EvaluationException { + public @Nullable Object getValue() throws EvaluationException { CompiledExpression compiledAst = this.compiledAst; if (compiledAst != null) { try { @@ -146,8 +144,7 @@ public Object getValue() throws EvaluationException { @SuppressWarnings("unchecked") @Override - @Nullable - public T getValue(@Nullable Class expectedResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Class expectedResultType) throws EvaluationException { CompiledExpression compiledAst = this.compiledAst; if (compiledAst != null) { try { @@ -182,8 +179,7 @@ public T getValue(@Nullable Class expectedResultType) throws EvaluationEx } @Override - @Nullable - public Object getValue(@Nullable Object rootObject) throws EvaluationException { + public @Nullable Object getValue(@Nullable Object rootObject) throws EvaluationException { CompiledExpression compiledAst = this.compiledAst; if (compiledAst != null) { try { @@ -211,8 +207,7 @@ public Object getValue(@Nullable Object rootObject) throws EvaluationException { @SuppressWarnings("unchecked") @Override - @Nullable - public T getValue(@Nullable Object rootObject, @Nullable Class expectedResultType) throws EvaluationException { + public @Nullable T getValue(@Nullable Object rootObject, @Nullable Class expectedResultType) throws EvaluationException { CompiledExpression compiledAst = this.compiledAst; if (compiledAst != null) { try { @@ -247,8 +242,7 @@ public T getValue(@Nullable Object rootObject, @Nullable Class expectedRe } @Override - @Nullable - public Object getValue(EvaluationContext context) throws EvaluationException { + public @Nullable Object getValue(EvaluationContext context) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); CompiledExpression compiledAst = this.compiledAst; @@ -277,8 +271,7 @@ public Object getValue(EvaluationContext context) throws EvaluationException { @SuppressWarnings("unchecked") @Override - @Nullable - public T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { + public @Nullable T getValue(EvaluationContext context, @Nullable Class expectedResultType) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); CompiledExpression compiledAst = this.compiledAst; @@ -312,8 +305,7 @@ public T getValue(EvaluationContext context, @Nullable Class expectedResu } @Override - @Nullable - public Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + public @Nullable Object getValue(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); CompiledExpression compiledAst = this.compiledAst; @@ -342,8 +334,7 @@ public Object getValue(EvaluationContext context, @Nullable Object rootObject) t @SuppressWarnings("unchecked") @Override - @Nullable - public T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class expectedResultType) + public @Nullable T getValue(EvaluationContext context, @Nullable Object rootObject, @Nullable Class expectedResultType) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); @@ -379,20 +370,17 @@ public T getValue(EvaluationContext context, @Nullable Object rootObject, @N } @Override - @Nullable - public Class getValueType() throws EvaluationException { + public @Nullable Class getValueType() throws EvaluationException { return getValueType(getEvaluationContext()); } @Override - @Nullable - public Class getValueType(@Nullable Object rootObject) throws EvaluationException { + public @Nullable Class getValueType(@Nullable Object rootObject) throws EvaluationException { return getValueType(getEvaluationContext(), rootObject); } @Override - @Nullable - public Class getValueType(EvaluationContext context) throws EvaluationException { + public @Nullable Class getValueType(EvaluationContext context) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); ExpressionState expressionState = new ExpressionState(context, this.configuration); TypeDescriptor typeDescriptor = this.ast.getValueInternal(expressionState).getTypeDescriptor(); @@ -400,38 +388,33 @@ public Class getValueType(EvaluationContext context) throws EvaluationExcepti } @Override - @Nullable - public Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { + public @Nullable Class getValueType(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { ExpressionState expressionState = new ExpressionState(context, toTypedValue(rootObject), this.configuration); TypeDescriptor typeDescriptor = this.ast.getValueInternal(expressionState).getTypeDescriptor(); return (typeDescriptor != null ? typeDescriptor.getType() : null); } @Override - @Nullable - public TypeDescriptor getValueTypeDescriptor() throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor() throws EvaluationException { return getValueTypeDescriptor(getEvaluationContext()); } @Override - @Nullable - public TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor(@Nullable Object rootObject) throws EvaluationException { ExpressionState expressionState = new ExpressionState(getEvaluationContext(), toTypedValue(rootObject), this.configuration); return this.ast.getValueInternal(expressionState).getTypeDescriptor(); } @Override - @Nullable - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { + public @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); ExpressionState expressionState = new ExpressionState(context, this.configuration); return this.ast.getValueInternal(expressionState).getTypeDescriptor(); } @Override - @Nullable - public TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) + public @Nullable TypeDescriptor getValueTypeDescriptor(EvaluationContext context, @Nullable Object rootObject) throws EvaluationException { Assert.notNull(context, "EvaluationContext must not be null"); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java index ee64ba4da2e0..e122017ed2e5 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpressionParser.java @@ -16,11 +16,12 @@ package org.springframework.expression.spel.standard; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.ParseException; import org.springframework.expression.ParserContext; import org.springframework.expression.common.TemplateAwareExpressionParser; import org.springframework.expression.spel.SpelParserConfiguration; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java index 61ccc8267fc0..f56a57193c3f 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/Token.java @@ -16,7 +16,7 @@ package org.springframework.expression.spel.standard; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Holder for a kind of token, the associated data, and its position in the input @@ -29,8 +29,7 @@ class Token { final TokenKind kind; - @Nullable - final String data; + final @Nullable String data; final int startPos; @@ -55,7 +54,7 @@ class Token { * @param startPos the exact start position * @param endPos the index of the last character */ - Token(TokenKind tokenKind, @Nullable char[] tokenData, int startPos, int endPos) { + Token(TokenKind tokenKind, char @Nullable [] tokenData, int startPos, int endPos) { this.kind = tokenKind; this.data = (tokenData != null ? new String(tokenData) : null); this.startPos = startPos; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java index e26fe15427b9..b9ac97ce14e3 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/standard/package-info.java @@ -1,9 +1,7 @@ /** * SpEL's standard parser implementation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.expression.spel.standard; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java index 120bb0806173..9684ce90850d 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/DataBindingMethodResolver.java @@ -20,11 +20,12 @@ import java.lang.reflect.Modifier; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.MethodExecutor; -import org.springframework.lang.Nullable; /** * An {@link org.springframework.expression.MethodResolver} variant for data binding @@ -46,8 +47,7 @@ private DataBindingMethodResolver() { } @Override - @Nullable - public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + public @Nullable MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List argumentTypes) throws AccessException { if (targetObject instanceof Class) { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java index bb230b6dd9f5..89bcaecad590 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectionHelper.java @@ -24,13 +24,14 @@ import java.util.List; import java.util.Optional; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.SpelEvaluationException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -57,8 +58,7 @@ public abstract class ReflectionHelper { * @return an {@code ArgumentsMatchInfo} object indicating what kind of match it was, * or {@code null} if it was not a match */ - @Nullable - static ArgumentsMatchKind compareArguments( + static @Nullable ArgumentsMatchKind compareArguments( List expectedArgTypes, List suppliedArgTypes, TypeConverter typeConverter) { Assert.isTrue(expectedArgTypes.size() == suppliedArgTypes.size(), @@ -146,8 +146,7 @@ else if (ClassUtils.isAssignable(paramTypeClazz, superClass)) { * @return an {@code ArgumentsMatchKind} object indicating what kind of match it was, * or {@code null} if it was not a match */ - @Nullable - static ArgumentsMatchKind compareArgumentsVarargs( + static @Nullable ArgumentsMatchKind compareArgumentsVarargs( List expectedArgTypes, List suppliedArgTypes, TypeConverter typeConverter) { Assert.isTrue(!CollectionUtils.isEmpty(expectedArgTypes), @@ -251,7 +250,7 @@ else if (typeConverter.canConvert(suppliedArg, TypeDescriptor.valueOf(varargsCom * @return {@code true} if some kind of conversion occurred on an argument * @throws SpelEvaluationException if a problem occurs during conversion */ - public static boolean convertAllArguments(TypeConverter converter, Object[] arguments, Method method) + public static boolean convertAllArguments(TypeConverter converter, @Nullable Object[] arguments, Method method) throws SpelEvaluationException { Integer varargsPosition = (method.isVarArgs() ? method.getParameterCount() - 1 : null); @@ -270,7 +269,8 @@ public static boolean convertAllArguments(TypeConverter converter, Object[] argu * @return {@code true} if some kind of conversion occurred on an argument * @throws EvaluationException if a problem occurs during conversion */ - static boolean convertArguments(TypeConverter converter, Object[] arguments, Executable executable, + @SuppressWarnings("NullAway") // Dataflow analysis limitation + static boolean convertArguments(TypeConverter converter, @Nullable Object[] arguments, Executable executable, @Nullable Integer varargsPosition) throws EvaluationException { boolean conversionOccurred = false; @@ -360,7 +360,8 @@ else if (!sourceType.isAssignableTo(componentTypeDesc) || * @throws EvaluationException if a problem occurs during conversion * @since 6.1 */ - public static boolean convertAllMethodHandleArguments(TypeConverter converter, Object[] arguments, + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public static boolean convertAllMethodHandleArguments(TypeConverter converter, @Nullable Object[] arguments, MethodHandle methodHandle, @Nullable Integer varargsPosition) throws EvaluationException { boolean conversionOccurred = false; @@ -454,7 +455,7 @@ else if (!sourceType.isAssignableTo(varargsComponentType) || * @param possibleArray an array object that may have the supplied value as the first element * @return true if the supplied value is the first entry in the array */ - private static boolean isFirstEntryInArray(Object value, @Nullable Object possibleArray) { + private static boolean isFirstEntryInArray(@Nullable Object value, @Nullable Object possibleArray) { if (possibleArray == null) { return false; } @@ -478,7 +479,7 @@ private static boolean isFirstEntryInArray(Object value, @Nullable Object possib * @param args the arguments to be set up for the invocation * @return a repackaged array of arguments where any varargs setup has been performed */ - public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredParameterTypes, Object... args) { + public static @Nullable Object[] setupArgumentsForVarargsInvocation(Class[] requiredParameterTypes, @Nullable Object... args) { Assert.notEmpty(requiredParameterTypes, "Required parameter types array must not be empty"); int parameterCount = requiredParameterTypes.length; @@ -492,7 +493,7 @@ public static Object[] setupArgumentsForVarargsInvocation(Class[] requiredPar // Check if repackaging is needed... if (parameterCount != argumentCount || !lastRequiredParameterType.isInstance(lastArgument)) { // Create an array for the leading arguments plus the varargs array argument. - Object[] newArgs = new Object[parameterCount]; + @Nullable Object[] newArgs = new Object[parameterCount]; // Copy all leading arguments to the new array, omitting the varargs array argument. System.arraycopy(args, 0, newArgs, 0, newArgs.length - 1); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java index e77eaa3ac586..6e4d23f4d4e8 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorExecutor.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. @@ -18,11 +18,12 @@ import java.lang.reflect.Constructor; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.AccessException; import org.springframework.expression.ConstructorExecutor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -37,8 +38,7 @@ public class ReflectiveConstructorExecutor implements ConstructorExecutor { private final Constructor ctor; - @Nullable - private final Integer varargsPosition; + private final @Nullable Integer varargsPosition; public ReflectiveConstructorExecutor(Constructor ctor) { @@ -52,7 +52,7 @@ public ReflectiveConstructorExecutor(Constructor ctor) { } @Override - public TypedValue execute(EvaluationContext context, Object... arguments) throws AccessException { + public TypedValue execute(EvaluationContext context, @Nullable Object... arguments) throws AccessException { try { ReflectionHelper.convertArguments( context.getTypeConverter(), arguments, this.ctor, this.varargsPosition); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java index ea2c5b054ad9..d5039e6a604c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveConstructorResolver.java @@ -22,6 +22,8 @@ import java.util.Comparator; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; @@ -31,7 +33,6 @@ import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind; -import org.springframework.lang.Nullable; /** * A constructor resolver that uses reflection to locate the constructor that @@ -56,8 +57,7 @@ public class ReflectiveConstructorResolver implements ConstructorResolver { * */ @Override - @Nullable - public ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) + public @Nullable ConstructorExecutor resolve(EvaluationContext context, String typeName, List argumentTypes) throws AccessException { try { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveIndexAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveIndexAccessor.java index 601f2792c290..011557ae46ff 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveIndexAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveIndexAccessor.java @@ -19,6 +19,8 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.expression.EvaluationContext; import org.springframework.expression.IndexAccessor; @@ -26,7 +28,6 @@ import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.CompilableIndexAccessor; import org.springframework.expression.spel.SpelNode; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -114,8 +115,7 @@ public class ReflectiveIndexAccessor implements CompilableIndexAccessor { private final Method readMethodToInvoke; - @Nullable - private final Method writeMethodToInvoke; + private final @Nullable Method writeMethodToInvoke; /** diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java index b66312328f93..759139d6d435 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodExecutor.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,13 +19,14 @@ import java.lang.reflect.Method; import java.lang.reflect.Modifier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.MethodExecutor; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -46,11 +47,9 @@ public class ReflectiveMethodExecutor implements MethodExecutor { */ private final Method methodToInvoke; - @Nullable - private final Integer varargsPosition; + private final @Nullable Integer varargsPosition; - @Nullable - private final Class publicDeclaringClass; + private final @Nullable Class publicDeclaringClass; private boolean argumentConversionOccurred = false; @@ -93,8 +92,7 @@ public final Method getMethod() { * @return the public class or interface that declares the method, or {@code null} if * no such public type could be found */ - @Nullable - public Class getPublicDeclaringClass() { + public @Nullable Class getPublicDeclaringClass() { return this.publicDeclaringClass; } @@ -104,7 +102,7 @@ public boolean didArgumentConversionOccur() { @Override - public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException { + public TypedValue execute(EvaluationContext context, Object target, @Nullable Object... arguments) throws AccessException { try { this.argumentConversionOccurred = ReflectionHelper.convertArguments( context.getTypeConverter(), arguments, this.originalMethod, this.varargsPosition); diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java index 5ee90278734b..df3dbd3e85e4 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectiveMethodResolver.java @@ -27,6 +27,8 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.BridgeMethodResolver; import org.springframework.core.MethodParameter; import org.springframework.core.convert.TypeDescriptor; @@ -40,7 +42,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; import org.springframework.expression.spel.support.ReflectionHelper.ArgumentsMatchKind; -import org.springframework.lang.Nullable; /** * Reflection-based {@link MethodResolver} used by default in {@link StandardEvaluationContext} @@ -59,8 +60,7 @@ public class ReflectiveMethodResolver implements MethodResolver { // more closely following the Java rules. private final boolean useDistance; - @Nullable - private Map, MethodFilter> filters; + private @Nullable Map, MethodFilter> filters; public ReflectiveMethodResolver() { @@ -113,8 +113,7 @@ public void registerMethodFilter(Class type, @Nullable MethodFilter filter) { * */ @Override - @Nullable - public MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, + public @Nullable MethodExecutor resolve(EvaluationContext context, Object targetObject, String name, List argumentTypes) throws AccessException { try { diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java index 9139d08cb4fa..bb9bb7cd9314 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/ReflectivePropertyAccessor.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,6 +33,7 @@ import kotlin.reflect.KProperty; import kotlin.reflect.full.KClasses; import kotlin.reflect.jvm.ReflectJvmMapping; +import org.jspecify.annotations.Nullable; import org.springframework.asm.MethodVisitor; import org.springframework.core.KotlinDetector; @@ -46,7 +47,6 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.CodeFlow; import org.springframework.expression.spel.CompilablePropertyAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -109,8 +109,7 @@ public ReflectivePropertyAccessor(boolean allowWrite) { * Returns {@code null} which means this is a general purpose accessor. */ @Override - @Nullable - public Class[] getSpecificTargetClasses() { + public Class @Nullable [] getSpecificTargetClasses() { return null; } @@ -155,7 +154,7 @@ public boolean canRead(EvaluationContext context, @Nullable Object target, Strin } @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public TypedValue read(EvaluationContext context, @Nullable Object target, String name) throws AccessException { Assert.state(target != null, "Target must not be null"); Class type = (target instanceof Class clazz ? clazz : target.getClass()); @@ -329,8 +328,7 @@ public void write(EvaluationContext context, @Nullable Object target, String nam } - @Nullable - private TypeDescriptor getTypeDescriptor(EvaluationContext context, Object target, String name) { + private @Nullable TypeDescriptor getTypeDescriptor(EvaluationContext context, Object target, String name) { Class type = (target instanceof Class clazz ? clazz : target.getClass()); if (type.isArray() && name.equals("length")) { @@ -352,8 +350,7 @@ private TypeDescriptor getTypeDescriptor(EvaluationContext context, Object targe return typeDescriptor; } - @Nullable - private Method findGetterForProperty(String propertyName, Class clazz, Object target) { + private @Nullable Method findGetterForProperty(String propertyName, Class clazz, Object target) { boolean targetIsAClass = (target instanceof Class); Method method = findGetterForProperty(propertyName, clazz, targetIsAClass); if (method == null && targetIsAClass) { @@ -363,8 +360,7 @@ private Method findGetterForProperty(String propertyName, Class clazz, Object return method; } - @Nullable - private Method findSetterForProperty(String propertyName, Class clazz, Object target) { + private @Nullable Method findSetterForProperty(String propertyName, Class clazz, Object target) { Method method = findSetterForProperty(propertyName, clazz, target instanceof Class); // In contrast to findGetterForProperty(), we do not look for setters in // java.lang.Class as a fallback, since Class doesn't have any public setters. @@ -374,8 +370,7 @@ private Method findSetterForProperty(String propertyName, Class clazz, Object /** * Find a getter method for the specified property. */ - @Nullable - protected Method findGetterForProperty(String propertyName, Class clazz, boolean mustBeStatic) { + protected @Nullable Method findGetterForProperty(String propertyName, Class clazz, boolean mustBeStatic) { Method method = findMethodForProperty(getPropertyMethodSuffixes(propertyName), "get", clazz, mustBeStatic, 0, ANY_TYPES); if (method == null) { @@ -393,14 +388,12 @@ protected Method findGetterForProperty(String propertyName, Class clazz, bool /** * Find a setter method for the specified property. */ - @Nullable - protected Method findSetterForProperty(String propertyName, Class clazz, boolean mustBeStatic) { + protected @Nullable Method findSetterForProperty(String propertyName, Class clazz, boolean mustBeStatic) { return findMethodForProperty(getPropertyMethodSuffixes(propertyName), "set", clazz, mustBeStatic, 1, ANY_TYPES); } - @Nullable - private Method findMethodForProperty(String[] methodSuffixes, String prefix, Class clazz, + private @Nullable Method findMethodForProperty(String[] methodSuffixes, String prefix, Class clazz, boolean mustBeStatic, int numberOfParams, Set> requiredReturnTypes) { Method[] methods = getSortedMethods(clazz); @@ -467,8 +460,7 @@ protected String getPropertyMethodSuffix(String propertyName) { return StringUtils.capitalize(propertyName); } - @Nullable - private Field findField(String name, Class clazz, Object target) { + private @Nullable Field findField(String name, Class clazz, Object target) { Field field = findField(name, clazz, target instanceof Class); if (field == null && target instanceof Class) { field = findField(name, target.getClass(), false); @@ -479,8 +471,7 @@ private Field findField(String name, Class clazz, Object target) { /** * Find a field of a certain name on a specified class. */ - @Nullable - protected Field findField(String name, Class clazz, boolean mustBeStatic) { + protected @Nullable Field findField(String name, Class clazz, boolean mustBeStatic) { Field[] fields = clazz.getFields(); for (Field field : fields) { if (field.getName().equals(name) && (!mustBeStatic || Modifier.isStatic(field.getModifiers()))) { @@ -516,7 +507,7 @@ protected Field findField(String name, Class clazz, boolean mustBeStatic) { *

    Note: An optimized accessor is currently only usable for read attempts. * Do not call this method if you need a read-write accessor. */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullable Object target, String name) { // Don't be clever for arrays or a null target... if (target == null) { @@ -567,8 +558,7 @@ public PropertyAccessor createOptimalAccessor(EvaluationContext context, @Nullab private static boolean isKotlinProperty(Method method, String methodSuffix) { Class clazz = method.getDeclaringClass(); - return KotlinDetector.isKotlinReflectPresent() && - KotlinDetector.isKotlinType(clazz) && + return KotlinDetector.isKotlinType(clazz) && KotlinDelegate.isKotlinProperty(method, methodSuffix); } @@ -619,8 +609,7 @@ private static class OptimalPropertyAccessor implements CompilablePropertyAccess } @Override - @Nullable - public Class[] getSpecificTargetClasses() { + public Class @Nullable [] getSpecificTargetClasses() { throw new UnsupportedOperationException("Should not be called on an OptimalPropertyAccessor"); } @@ -635,11 +624,12 @@ public boolean canRead(EvaluationContext context, @Nullable Object target, Strin } if (this.member instanceof Method method) { - String getterName = "get" + StringUtils.capitalize(name); + String capitalizedName = StringUtils.capitalize(name); + String getterName = "get" + capitalizedName; if (getterName.equals(method.getName())) { return true; } - getterName = "is" + StringUtils.capitalize(name); + getterName = "is" + capitalizedName; if (getterName.equals(method.getName())) { return true; } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java index 3a11e8277e55..ed1f8e076cc6 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/SimpleEvaluationContext.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.BeanResolver; @@ -37,7 +39,6 @@ import org.springframework.expression.TypedValue; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; /** * A basic implementation of {@link EvaluationContext} that focuses on a subset @@ -178,8 +179,7 @@ public List getMethodResolvers() { * @return always {@code null} */ @Override - @Nullable - public BeanResolver getBeanResolver() { + public @Nullable BeanResolver getBeanResolver() { return null; } @@ -257,8 +257,7 @@ public void setVariable(String name, @Nullable Object value) { * @return the value of the variable or function, or {@code null} if not found */ @Override - @Nullable - public Object lookupVariable(String name) { + public @Nullable Object lookupVariable(String name) { return this.variables.get(name); } @@ -344,11 +343,9 @@ public static final class Builder { private List resolvers = Collections.emptyList(); - @Nullable - private TypeConverter typeConverter; + private @Nullable TypeConverter typeConverter; - @Nullable - private TypedValue rootObject; + private @Nullable TypedValue rootObject; private boolean assignmentEnabled = true; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java index 5d196b7e930e..7dcca874ce19 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardEvaluationContext.java @@ -23,6 +23,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.TypeDescriptor; import org.springframework.expression.BeanResolver; import org.springframework.expression.ConstructorResolver; @@ -36,7 +38,6 @@ import org.springframework.expression.TypeConverter; import org.springframework.expression.TypeLocator; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -81,29 +82,21 @@ public class StandardEvaluationContext implements EvaluationContext { private TypedValue rootObject; - @Nullable - private volatile List propertyAccessors; + private volatile @Nullable List propertyAccessors; - @Nullable - private volatile List indexAccessors; + private volatile @Nullable List indexAccessors; - @Nullable - private volatile List constructorResolvers; + private volatile @Nullable List constructorResolvers; - @Nullable - private volatile List methodResolvers; + private volatile @Nullable List methodResolvers; - @Nullable - private volatile ReflectiveMethodResolver reflectiveMethodResolver; + private volatile @Nullable ReflectiveMethodResolver reflectiveMethodResolver; - @Nullable - private BeanResolver beanResolver; + private @Nullable BeanResolver beanResolver; - @Nullable - private TypeLocator typeLocator; + private @Nullable TypeLocator typeLocator; - @Nullable - private TypeConverter typeConverter; + private @Nullable TypeConverter typeConverter; private TypeComparator typeComparator = StandardTypeComparator.INSTANCE; @@ -248,8 +241,7 @@ public void setBeanResolver(BeanResolver beanResolver) { } @Override - @Nullable - public BeanResolver getBeanResolver() { + public @Nullable BeanResolver getBeanResolver() { return this.beanResolver; } @@ -400,8 +392,7 @@ public void registerFunction(String name, MethodHandle methodHandle) { * @return the value of the variable or function, or {@code null} if not found */ @Override - @Nullable - public Object lookupVariable(String name) { + public @Nullable Object lookupVariable(String name) { return this.variables.get(name); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java index 7b8d892b0d7e..21f7727953e1 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardOperatorOverloader.java @@ -16,10 +16,11 @@ package org.springframework.expression.spel.support; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationException; import org.springframework.expression.Operation; import org.springframework.expression.OperatorOverloader; -import org.springframework.lang.Nullable; /** * Standard implementation of {@link OperatorOverloader}. diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java index fc913470dc44..aa6bcb8c513a 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeComparator.java @@ -19,10 +19,11 @@ import java.math.BigDecimal; import java.math.BigInteger; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.TypeComparator; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.NumberUtils; diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java index eeb1c09397db..9d57d9bb4650 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeConverter.java @@ -18,6 +18,8 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; @@ -25,7 +27,6 @@ import org.springframework.expression.TypeConverter; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -76,8 +77,7 @@ public boolean canConvert(@Nullable TypeDescriptor sourceType, TypeDescriptor ta } @Override - @Nullable - public Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable Object convertValue(@Nullable Object value, @Nullable TypeDescriptor sourceType, TypeDescriptor targetType) { try { return this.conversionService.get().convert(value, sourceType, targetType); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java index 816b66cb71c5..1ad740b02d2c 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/StandardTypeLocator.java @@ -22,12 +22,13 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.core.SmartClassLoader; import org.springframework.expression.EvaluationException; import org.springframework.expression.TypeLocator; import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.SpelMessage; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -46,8 +47,7 @@ */ public class StandardTypeLocator implements TypeLocator { - @Nullable - private final ClassLoader classLoader; + private final @Nullable ClassLoader classLoader; private final List importPrefixes = new ArrayList<>(1); @@ -129,8 +129,7 @@ public Class findType(String typeName) throws EvaluationException { throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName); } - @Nullable - private Class loadType(String typeName) { + private @Nullable Class loadType(String typeName) { try { return ClassUtils.forName(typeName, this.classLoader); } diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java b/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java index d77cb741fa54..7c9c08fd9cea 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/support/package-info.java @@ -1,9 +1,7 @@ /** * SpEL's default implementations for various core abstractions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.expression.spel.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java index 6aede670ad58..a909159c06fd 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ComparatorTests.java @@ -18,6 +18,7 @@ import java.math.BigDecimal; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.expression.EvaluationException; @@ -27,7 +28,6 @@ import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeComparator; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java index 3bbb9411f80e..f32b3a06325a 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/CompilableMapAccessor.java @@ -18,11 +18,12 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.asm.MethodVisitor; import org.springframework.expression.AccessException; import org.springframework.expression.EvaluationContext; import org.springframework.expression.TypedValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java index f827eab9e9ca..d2957bab9a4c 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ExpressionStateTests.java @@ -16,8 +16,6 @@ package org.springframework.expression.spel; -import java.util.Map; - import org.junit.jupiter.api.Test; import org.springframework.core.convert.TypeDescriptor; @@ -55,21 +53,6 @@ void construction() { assertThat(state.getEvaluationContext()).isEqualTo(context); } - @Test - @SuppressWarnings("removal") - void localVariables() { - Object value = state.lookupLocalVariable("foo"); - assertThat(value).isNull(); - - state.setLocalVariable("foo",34); - value = state.lookupLocalVariable("foo"); - assertThat(value).isEqualTo(34); - - state.setLocalVariable("foo", null); - value = state.lookupLocalVariable("foo"); - assertThat(value).isNull(); - } - @Test void globalVariables() { TypedValue typedValue = state.lookupVariable("foo"); @@ -86,41 +69,6 @@ void globalVariables() { assertThat(typedValue.getTypeDescriptor().getType()).isEqualTo(String.class); } - @Test - @SuppressWarnings("removal") - void noVariableInterference() { - TypedValue typedValue = state.lookupVariable("foo"); - assertThat(typedValue).isEqualTo(TypedValue.NULL); - - state.setLocalVariable("foo",34); - typedValue = state.lookupVariable("foo"); - assertThat(typedValue).isEqualTo(TypedValue.NULL); - - state.setVariable("goo", "hello"); - assertThat(state.lookupLocalVariable("goo")).isNull(); - } - - @Test - @SuppressWarnings("removal") - void localVariableNestedScopes() { - assertThat(state.lookupLocalVariable("foo")).isNull(); - - state.setLocalVariable("foo",12); - assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); - - state.enterScope(null); - // found in upper scope - assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); - - state.setLocalVariable("foo","abc"); - // found in nested scope - assertThat(state.lookupLocalVariable("foo")).isEqualTo("abc"); - - state.exitScope(); - // found in nested scope - assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); - } - @Test void rootContextObject() { assertThat(state.getRootContextObject().getValue().getClass()).isEqualTo(Inventor.class); @@ -159,25 +107,6 @@ void activeContextObject() { assertThat(state.getActiveContextObject()).isEqualTo(TypedValue.NULL); } - @Test - @SuppressWarnings("removal") - void populatedNestedScopes() { - assertThat(state.lookupLocalVariable("foo")).isNull(); - - state.enterScope("foo",34); - assertThat(state.lookupLocalVariable("foo")).isEqualTo(34); - - state.enterScope(null); - state.setLocalVariable("foo", 12); - assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); - - state.exitScope(); - assertThat(state.lookupLocalVariable("foo")).isEqualTo(34); - - state.exitScope(); - assertThat(state.lookupLocalVariable("goo")).isNull(); - } - @Test void rootObjectConstructor() { EvaluationContext ctx = TestScenarioCreator.getTestEvaluationContext(); @@ -189,27 +118,6 @@ void rootObjectConstructor() { assertThat(stateRoot.getValue()).isEqualTo("i am a string"); } - @Test - @SuppressWarnings("removal") - void populatedNestedScopesMap() { - assertThat(state.lookupLocalVariable("foo")).isNull(); - assertThat(state.lookupLocalVariable("goo")).isNull(); - - state.enterScope(Map.of("foo", 34, "goo", "abc")); - assertThat(state.lookupLocalVariable("foo")).isEqualTo(34); - assertThat(state.lookupLocalVariable("goo")).isEqualTo("abc"); - - state.enterScope(null); - state.setLocalVariable("foo",12); - assertThat(state.lookupLocalVariable("foo")).isEqualTo(12); - assertThat(state.lookupLocalVariable("goo")).isEqualTo("abc"); - - state.exitScope(); - state.exitScope(); - assertThat(state.lookupLocalVariable("foo")).isNull(); - assertThat(state.lookupLocalVariable("goo")).isNull(); - } - @Test void operators() { assertThatExceptionOfType(SpelEvaluationException.class) 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 e205f7c94dfc..3fb1b2cbfe20 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; - - static { - listOfString.add("1"); - listOfString.add("2"); - listOfString.add("3"); - listOfInteger.add(4); - listOfInteger.add(5); - listOfInteger.add(6); - } + private static final List listOfString = List.of("1", "2", "3"); + private static final List listOfInteger = List.of(4, 5, 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(); - - // 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(); - - // 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(); + @BeforeAll + @SuppressWarnings("unchecked") + static void verifyConversionsAreSupportedByStandardTypeConverter() { + StandardEvaluationContext evaluationContext = new StandardEvaluationContext(); + TypeConverter typeConverter = evaluationContext.getTypeConverter(); + + // List to List + assertThat(typeDescriptorForListOfString.getElementTypeDescriptor().getType()) + .isEqualTo(String.class); + List strings = (List) typeConverter.convertValue(listOfInteger, + typeDescriptorForListOfInteger, typeDescriptorForListOfString); + assertThat(strings).containsExactly("4", "5", "6"); + + // 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 @@ void testConvert() { 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 @@ public Collection getFoos() { } 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/IndexingTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java index 52b9e4c0203e..763589f0fe8e 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/IndexingTests.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 com.fasterxml.jackson.databind.node.NullNode; import com.fasterxml.jackson.databind.node.TextNode; import example.Color; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -51,14 +52,13 @@ import org.springframework.expression.spel.support.SimpleEvaluationContext; import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.testresources.Person; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.doThrow; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/OptionalNullSafetyTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/OptionalNullSafetyTests.java new file mode 100644 index 000000000000..ef5ec0a43fa3 --- /dev/null +++ b/spring-expression/src/test/java/org/springframework/expression/spel/OptionalNullSafetyTests.java @@ -0,0 +1,388 @@ +/* + * Copyright 2002-2025 the original author 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.expression.spel; + +import java.util.List; +import java.util.Optional; + +import org.jspecify.annotations.Nullable; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import org.springframework.expression.Expression; +import org.springframework.expression.spel.standard.SpelExpressionParser; +import org.springframework.expression.spel.support.StandardEvaluationContext; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.springframework.expression.spel.SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE; +import static org.springframework.expression.spel.SpelMessage.PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL; + +/** + * Tests which verify support for using {@link Optional} with the null-safe and + * Elvis operators in SpEL expressions. + * + * @author Sam Brannen + * @since 7.0 + */ +class OptionalNullSafetyTests { + + private final SpelExpressionParser parser = new SpelExpressionParser(); + + private final StandardEvaluationContext context = new StandardEvaluationContext(); + + + @BeforeEach + void setUpContext() { + context.setVariable("service", new Service()); + } + + + /** + * Tests for the status quo when using {@link Optional} in SpEL expressions, + * before explicit null-safe support was added in 7.0. + */ + @Nested + class LegacyOptionalTests { + + @Test + void accessPropertyOnNullOptional() { + Expression expr = parser.parseExpression("#service.findJediByName(null).empty"); + + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expr.getValue(context)) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(PROPERTY_OR_FIELD_NOT_READABLE_ON_NULL); + assertThat(ex).hasMessageContaining("Property or field 'empty' cannot be found on null"); + }); + } + + @Test + void accessPropertyOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName(null)?.empty"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void invokeMethodOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName(null)?.salutation('Master')"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void accessIndexOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor(null)?.[1]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void projectionOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor(null)?.![#this.length]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void selectAllOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor(null)?.?[#this.length > 5]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void selectFirstOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor(null)?.^[#this.length > 5]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void selectLastOnNullOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor(null)?.$[#this.length > 5]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void elvisOperatorOnNullOptional() { + Expression expr = parser.parseExpression("#service.findJediByName(null) ?: 'unknown'"); + + assertThat(expr.getValue(context)).isEqualTo("unknown"); + } + + @Test + void accessNonexistentPropertyOnEmptyOptional() { + assertPropertyNotReadable("#service.findJediByName('').name"); + } + + @Test + void accessNonexistentPropertyOnNonEmptyOptional() { + assertPropertyNotReadable("#service.findJediByName('Yoda').name"); + } + + @Test + void accessOptionalPropertyOnEmptyOptional() { + Expression expr = parser.parseExpression("#service.findJediByName('').present"); + + assertThat(expr.getValue(context, Boolean.class)).isFalse(); + } + + @Test + void accessOptionalPropertyOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('')?.present"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context, Boolean.class)).isFalse(); + assertThat(expr.getValue(context, Boolean.class)).isFalse(); + } + + @Test + void accessOptionalPropertyOnNonEmptyOptional() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda').present"); + + assertThat(expr.getValue(context, Boolean.class)).isTrue(); + } + + @Test + void accessOptionalPropertyOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda')?.present"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context, Boolean.class)).isTrue(); + assertThat(expr.getValue(context, Boolean.class)).isTrue(); + } + + @Test + void invokeOptionalMethodOnEmptyOptional() { + Expression expr = parser.parseExpression("#service.findJediByName('').orElse('Luke')"); + + assertThat(expr.getValue(context)).isEqualTo("Luke"); + } + + @Test + void invokeOptionalMethodOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('')?.orElse('Luke')"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isEqualTo("Luke"); + assertThat(expr.getValue(context)).isEqualTo("Luke"); + } + + @Test + void invokeOptionalMethodOnNonEmptyOptional() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda').orElse('Luke')"); + + assertThat(expr.getValue(context)).isEqualTo(new Jedi("Yoda")); + } + + @Test + void invokeOptionalMethodOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda')?.orElse('Luke')"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isEqualTo(new Jedi("Yoda")); + assertThat(expr.getValue(context)).isEqualTo(new Jedi("Yoda")); + } + + private void assertPropertyNotReadable(String expression) { + Expression expr = parser.parseExpression(expression); + + assertThatExceptionOfType(SpelEvaluationException.class) + .isThrownBy(() -> expr.getValue(context)) + .satisfies(ex -> { + assertThat(ex.getMessageCode()).isEqualTo(PROPERTY_OR_FIELD_NOT_READABLE); + assertThat(ex).hasMessageContaining("Property or field 'name' cannot be found on object of type 'java.util.Optional'"); + }); + } + + } + + @Nested + class NullSafeTests { + + @Test + void accessPropertyOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('')?.name"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isNull(); + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void accessPropertyOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda')?.name"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isEqualTo("Yoda"); + assertThat(expr.getValue(context)).isEqualTo("Yoda"); + } + + @Test + void invokeMethodOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('')?.salutation('Master')"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isNull(); + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void invokeMethodOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda')?.salutation('Master')"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isEqualTo("Master Yoda"); + assertThat(expr.getValue(context)).isEqualTo("Master Yoda"); + } + + @Test + void accessIndexOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.[1]"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isNull(); + assertThat(expr.getValue(context)).isNull(); + } + + @Test + void accessIndexOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.[1]"); + + // Invoke multiple times to ensure there are no caching issues. + assertThat(expr.getValue(context)).isEqualTo("lemon"); + assertThat(expr.getValue(context)).isEqualTo("lemon"); + } + + @Test + void projectionOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.![#this.length]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void projectionOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.![#this.length]"); + + assertThat(expr.getValue(context, List.class)).containsExactly(6, 5, 5, 9); + } + + @Test + void selectAllOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.?[#this.length > 5]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void selectAllOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.?[#this.length > 5]"); + + assertThat(expr.getValue(context, List.class)).containsExactly("banana", "pineapple"); + } + + @Test + void selectFirstOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.^[#this.length > 5]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void selectFirstOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.^[#this.length > 5]"); + + assertThat(expr.getValue(context, List.class)).containsExactly("banana"); + } + + @Test + void selectLastOnEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('')?.$[#this.length > 5]"); + + assertThat(expr.getValue(context)).isNull(); + } + + @Test + @SuppressWarnings("unchecked") + void selectLastOnNonEmptyOptionalViaNullSafeOperator() { + Expression expr = parser.parseExpression("#service.findFruitsByColor('yellow')?.$[#this.length > 5]"); + + assertThat(expr.getValue(context, List.class)).containsExactly("pineapple"); + } + + } + + @Nested + class ElvisTests { + + @Test + void elvisOperatorOnEmptyOptional() { + Expression expr = parser.parseExpression("#service.findJediByName('') ?: 'unknown'"); + + assertThat(expr.getValue(context)).isEqualTo("unknown"); + } + + @Test + void elvisOperatorOnNonEmptyOptional() { + Expression expr = parser.parseExpression("#service.findJediByName('Yoda') ?: 'unknown'"); + + assertThat(expr.getValue(context)).isEqualTo(new Jedi("Yoda")); + } + + } + + + record Jedi(String name) { + + public String salutation(String salutation) { + return salutation + " " + this.name; + } + } + + static class Service { + + public Optional findJediByName(@Nullable String name) { + if (name == null) { + return null; + } + if (name.isEmpty()) { + return Optional.empty(); + } + return Optional.of(new Jedi(name)); + } + + public Optional> findFruitsByColor(@Nullable String color) { + if (color == null) { + return null; + } + if (color.isEmpty()) { + return Optional.empty(); + } + return Optional.of(List.of("banana", "lemon", "mango", "pineapple")); + } + + } + +} diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index a05e52ccb281..0bae62c0dc84 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -36,6 +36,7 @@ import example.Color; import example.FruitMap; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; @@ -60,7 +61,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.testdata.PersonInOtherPackage; import org.springframework.expression.spel.testresources.Person; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static java.util.stream.Collectors.joining; diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java index 57cc2168ea15..70ec1c875908 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelDocumentationTests.java @@ -737,8 +737,8 @@ void ternary() { parser.parseExpression("Name").setValue(societyContext, "IEEE"); societyContext.setVariable("queryName", "Nikola Tesla"); - String expression = "isMember(#queryName) ? #queryName + ' is a member of the ' " - + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; + String expression = "isMember(#queryName) ? #queryName + ' is a member of the ' " + + "+ Name + ' Society' : #queryName + ' is not a member of the ' + Name + ' Society'"; String queryResultString = parser.parseExpression(expression).getValue(societyContext, String.class); assertThat(queryResultString).isEqualTo("Nikola Tesla is a member of the IEEE Society"); diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java index 5e7d4987da53..3ec7587e0c48 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelReproTests.java @@ -37,6 +37,7 @@ import java.util.concurrent.atomic.AtomicInteger; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; @@ -61,7 +62,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeLocator; import org.springframework.expression.spel.testresources.le.div.mod.reserved.Reserver; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -1169,7 +1169,6 @@ void SPR9994_bridgeMethods() throws Exception { TypedValue value = accessor.read(context, target, "property"); assertThat(value.getValue()).isEqualTo(1); assertThat(value.getTypeDescriptor().getType()).isEqualTo(Integer.class); - assertThat(value.getTypeDescriptor().getAnnotations()).isNotEmpty(); } @Test @@ -2158,8 +2157,7 @@ public void setProperty(Integer value) { } @Override - @Nullable - public Integer getProperty() { + public @Nullable Integer getProperty() { return this.value; } } 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 4e5e22df0c59..b0178d144a73 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 @@ -18,6 +18,7 @@ import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,7 +31,6 @@ import org.springframework.expression.spel.support.StandardEvaluationContext; import org.springframework.expression.spel.support.StandardTypeConverter; import org.springframework.expression.spel.support.StandardTypeLocator; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -344,15 +344,13 @@ record ArrayHolder(String... array) { static class ArrayHolderConverter implements GenericConverter { - @Nullable @Override - public Set getConvertibleTypes() { + public @Nullable Set getConvertibleTypes() { return Set.of(new ConvertiblePair(ArrayHolder.class, Object[].class)); } - @Nullable @Override - public String[] convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { + public @Nullable String[] convert(@Nullable Object source, TypeDescriptor sourceType, TypeDescriptor targetType) { return ((ArrayHolder) source).array(); } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/ast/AccessorUtilsTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/ast/AccessorUtilsTests.java index 659a99a87adf..23e1978e7088 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/ast/AccessorUtilsTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/ast/AccessorUtilsTests.java @@ -18,10 +18,10 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.expression.TargetedAccessor; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; @@ -102,8 +102,7 @@ private static TargetedAccessor createAccessor(String name, Class type) { private record DemoAccessor(String name, Class[] types) implements TargetedAccessor { @Override - @Nullable - public Class[] getSpecificTargetClasses() { + public Class @Nullable [] getSpecificTargetClasses() { return this.types; } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java index 945cc4e7c6f3..a99072ac7c18 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/testresources/PlaceOfBirth.java @@ -16,7 +16,7 @@ package org.springframework.expression.spel.testresources; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; public class PlaceOfBirth { diff --git a/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt b/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt index 24fc744adf85..a88dde59f667 100644 --- a/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt +++ b/spring-expression/src/test/kotlin/org/springframework/expression/spel/SpelReproKotlinTests.kt @@ -40,9 +40,9 @@ class SpelReproKotlinTests { val expr = parser.parseExpression("#key.startsWith('hello')") context.registerFunction("get", Config::class.java.getMethod("get", String::class.java)) context.setVariable("key", "hello world") - assertThat(expr.getValue(context, Boolean::class.java)).isTrue() + assertThat(expr.getValue(context, Boolean::class.java)!!).isTrue() context.setVariable("key", "") - assertThat(expr.getValue(context, Boolean::class.java)).isFalse() + assertThat(expr.getValue(context, Boolean::class.java)!!).isFalse() } @Test @@ -50,23 +50,23 @@ class SpelReproKotlinTests { val expr = parser.parseExpression("#key.startsWith('hello')") context.registerFunction("suspendingGet", Config::class.java.getMethod("suspendingGet", String::class.java, Continuation::class.java)) context.setVariable("key", "hello world") - assertThat(expr.getValue(context, Boolean::class.java)).isTrue() + assertThat(expr.getValue(context, Boolean::class.java)!!).isTrue() context.setVariable("key", "") - assertThat(expr.getValue(context, Boolean::class.java)).isFalse() + assertThat(expr.getValue(context, Boolean::class.java)!!).isFalse() } @Test fun `gh-30468 Unmangle Kotlin inlined class getter`() { context.setVariable("something", Something(UUID(123), "name")) val expr = parser.parseExpression("#something.id") - assertThat(expr.getValue(context, Int::class.java)).isEqualTo(123) + assertThat(expr.getValue(context, Int::class.java)!!).isEqualTo(123) } @Test fun `gh-30468 Unmangle Kotlin inlined class setter`() { context.setVariable("something", Something(UUID(123), "name")) val expr = parser.parseExpression("#something.id = 456") - assertThat(expr.getValue(context, Int::class.java)).isEqualTo(456) + assertThat(expr.getValue(context, Int::class.java)!!).isEqualTo(456) } @Suppress("UNUSED_PARAMETER") diff --git a/spring-jcl/spring-jcl.gradle b/spring-jcl/spring-jcl.gradle deleted file mode 100644 index d609737b2551..000000000000 --- a/spring-jcl/spring-jcl.gradle +++ /dev/null @@ -1,6 +0,0 @@ -description = "Spring Commons Logging Bridge" - -dependencies { - optional("org.apache.logging.log4j:log4j-api") - optional("org.slf4j:slf4j-api") -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/Log.java b/spring-jcl/src/main/java/org/apache/commons/logging/Log.java deleted file mode 100644 index b69914a12d6e..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/Log.java +++ /dev/null @@ -1,196 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.commons.logging; - -/** - * A simple logging interface abstracting logging APIs. In order to be - * instantiated successfully by {@link LogFactory}, classes that implement - * this interface must have a constructor that takes a single String - * parameter representing the "name" of this Log. - * - *

    The six logging levels used by Log are (in order): - *

      - *
    1. trace (the least serious)
    2. - *
    3. debug
    4. - *
    5. info
    6. - *
    7. warn
    8. - *
    9. error
    10. - *
    11. fatal (the most serious)
    12. - *
    - * - * The mapping of these log levels to the concepts used by the underlying - * logging system is implementation dependent. - * The implementation should ensure, though, that this ordering behaves - * as expected. - * - *

    Performance is often a logging concern. - * By examining the appropriate property, - * a component can avoid expensive operations (producing information - * to be logged). - * - *

    For example, - *

    - *    if (log.isDebugEnabled()) {
    - *        ... do something expensive ...
    - *        log.debug(theResult);
    - *    }
    - * 
    - * - *

    Configuration of the underlying logging system will generally be done - * external to the Logging APIs, through whatever mechanism is supported by - * that system. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - */ -public interface Log { - - /** - * Is fatal logging currently enabled? - *

    Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than fatal. - * @return true if fatal is enabled in the underlying logger. - */ - boolean isFatalEnabled(); - - /** - * Is error logging currently enabled? - *

    Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than error. - * @return true if error is enabled in the underlying logger. - */ - boolean isErrorEnabled(); - - /** - * Is warn logging currently enabled? - *

    Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than warn. - * @return true if warn is enabled in the underlying logger. - */ - boolean isWarnEnabled(); - - /** - * Is info logging currently enabled? - *

    Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than info. - * @return true if info is enabled in the underlying logger. - */ - boolean isInfoEnabled(); - - /** - * Is debug logging currently enabled? - *

    Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than debug. - * @return true if debug is enabled in the underlying logger. - */ - boolean isDebugEnabled(); - - /** - * Is trace logging currently enabled? - *

    Call this method to prevent having to perform expensive operations - * (for example, String concatenation) - * when the log level is more than trace. - * @return true if trace is enabled in the underlying logger. - */ - boolean isTraceEnabled(); - - - /** - * Logs a message with fatal log level. - * @param message log this message - */ - void fatal(Object message); - - /** - * Logs an error with fatal log level. - * @param message log this message - * @param t log this cause - */ - void fatal(Object message, Throwable t); - - /** - * Logs a message with error log level. - * @param message log this message - */ - void error(Object message); - - /** - * Logs an error with error log level. - * @param message log this message - * @param t log this cause - */ - void error(Object message, Throwable t); - - /** - * Logs a message with warn log level. - * @param message log this message - */ - void warn(Object message); - - /** - * Logs an error with warn log level. - * @param message log this message - * @param t log this cause - */ - void warn(Object message, Throwable t); - - /** - * Logs a message with info log level. - * @param message log this message - */ - void info(Object message); - - /** - * Logs an error with info log level. - * @param message log this message - * @param t log this cause - */ - void info(Object message, Throwable t); - - /** - * Logs a message with debug log level. - * @param message log this message - */ - void debug(Object message); - - /** - * Logs an error with debug log level. - * @param message log this message - * @param t log this cause - */ - void debug(Object message, Throwable t); - - /** - * Logs a message with trace log level. - * @param message log this message - */ - void trace(Object message); - - /** - * Logs an error with trace log level. - * @param message log this message - * @param t log this cause - */ - void trace(Object message, Throwable t); - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java deleted file mode 100644 index c7074fdc0363..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogAdapter.java +++ /dev/null @@ -1,698 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.apache.commons.logging; - -import java.io.Serializable; -import java.util.function.Function; -import java.util.logging.LogRecord; - -import org.apache.logging.log4j.Level; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.spi.ExtendedLogger; -import org.apache.logging.log4j.spi.LoggerContext; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.slf4j.spi.LocationAwareLogger; - -/** - * Spring's common JCL adapter behind {@link LogFactory} and {@link LogFactoryService}. - * Detects the presence of Log4j 2.x / SLF4J, falling back to {@code java.util.logging}. - * - * @author Juergen Hoeller - * @author Sebastien Deleuze - * @since 5.1 - */ -final class LogAdapter { - - private static final boolean log4jSpiPresent = isPresent("org.apache.logging.log4j.spi.ExtendedLogger"); - - private static final boolean log4jSlf4jProviderPresent = isPresent("org.apache.logging.slf4j.SLF4JProvider"); - - private static final boolean slf4jSpiPresent = isPresent("org.slf4j.spi.LocationAwareLogger"); - - private static final boolean slf4jApiPresent = isPresent("org.slf4j.Logger"); - - - private static final Function createLog; - - static { - if (log4jSpiPresent) { - if (log4jSlf4jProviderPresent && slf4jSpiPresent) { - // log4j-to-slf4j bridge -> we'll rather go with the SLF4J SPI; - // however, we still prefer Log4j over the plain SLF4J API since - // the latter does not have location awareness support. - createLog = Slf4jAdapter::createLocationAwareLog; - } - else { - // Use Log4j 2.x directly, including location awareness support - createLog = Log4jAdapter::createLog; - } - } - else if (slf4jSpiPresent) { - // Full SLF4J SPI including location awareness support - createLog = Slf4jAdapter::createLocationAwareLog; - } - else if (slf4jApiPresent) { - // Minimal SLF4J API without location awareness support - createLog = Slf4jAdapter::createLog; - } - else { - // java.util.logging as default - // Defensively use lazy-initializing adapter class here as well since the - // java.logging module is not present by default on JDK 9. We are requiring - // its presence if neither Log4j nor SLF4J is available; however, in the - // case of Log4j or SLF4J, we are trying to prevent early initialization - // of the JavaUtilLog adapter - for example, by a JVM in debug mode - when eagerly - // trying to parse the bytecode for all the cases of this switch clause. - createLog = JavaUtilAdapter::createLog; - } - } - - - private LogAdapter() { - } - - - /** - * Create an actual {@link Log} instance for the selected API. - * @param name the logger name - */ - public static Log createLog(String name) { - return createLog.apply(name); - } - - private static boolean isPresent(String className) { - try { - Class.forName(className, false, LogAdapter.class.getClassLoader()); - return true; - } - catch (Throwable ex) { - // Typically ClassNotFoundException or NoClassDefFoundError... - return false; - } - } - - - private static class Log4jAdapter { - - public static Log createLog(String name) { - return new Log4jLog(name); - } - } - - - private static class Slf4jAdapter { - - public static Log createLocationAwareLog(String name) { - Logger logger = LoggerFactory.getLogger(name); - return (logger instanceof LocationAwareLogger locationAwareLogger ? - new Slf4jLocationAwareLog(locationAwareLogger) : new Slf4jLog<>(logger)); - } - - public static Log createLog(String name) { - return new Slf4jLog<>(LoggerFactory.getLogger(name)); - } - } - - - private static class JavaUtilAdapter { - - public static Log createLog(String name) { - return new JavaUtilLog(name); - } - } - - - @SuppressWarnings("serial") - private static class Log4jLog implements Log, Serializable { - - private static final String FQCN = Log4jLog.class.getName(); - - private static final LoggerContext loggerContext = - LogManager.getContext(Log4jLog.class.getClassLoader(), false); - - private final String name; - - private final transient ExtendedLogger logger; - - public Log4jLog(String name) { - this.name = name; - LoggerContext context = loggerContext; - if (context == null) { - // Circular call in early-init scenario -> static field not initialized yet - context = LogManager.getContext(Log4jLog.class.getClassLoader(), false); - } - this.logger = context.getLogger(name); - } - - @Override - public boolean isFatalEnabled() { - return this.logger.isEnabled(Level.FATAL); - } - - @Override - public boolean isErrorEnabled() { - return this.logger.isEnabled(Level.ERROR); - } - - @Override - public boolean isWarnEnabled() { - return this.logger.isEnabled(Level.WARN); - } - - @Override - public boolean isInfoEnabled() { - return this.logger.isEnabled(Level.INFO); - } - - @Override - public boolean isDebugEnabled() { - return this.logger.isEnabled(Level.DEBUG); - } - - @Override - public boolean isTraceEnabled() { - return this.logger.isEnabled(Level.TRACE); - } - - @Override - public void fatal(Object message) { - log(Level.FATAL, message, null); - } - - @Override - public void fatal(Object message, Throwable exception) { - log(Level.FATAL, message, exception); - } - - @Override - public void error(Object message) { - log(Level.ERROR, message, null); - } - - @Override - public void error(Object message, Throwable exception) { - log(Level.ERROR, message, exception); - } - - @Override - public void warn(Object message) { - log(Level.WARN, message, null); - } - - @Override - public void warn(Object message, Throwable exception) { - log(Level.WARN, message, exception); - } - - @Override - public void info(Object message) { - log(Level.INFO, message, null); - } - - @Override - public void info(Object message, Throwable exception) { - log(Level.INFO, message, exception); - } - - @Override - public void debug(Object message) { - log(Level.DEBUG, message, null); - } - - @Override - public void debug(Object message, Throwable exception) { - log(Level.DEBUG, message, exception); - } - - @Override - public void trace(Object message) { - log(Level.TRACE, message, null); - } - - @Override - public void trace(Object message, Throwable exception) { - log(Level.TRACE, message, exception); - } - - private void log(Level level, Object message, Throwable exception) { - if (message instanceof String text) { - // Explicitly pass a String argument, avoiding Log4j's argument expansion - // for message objects in case of "{}" sequences (SPR-16226) - if (exception != null) { - this.logger.logIfEnabled(FQCN, level, null, text, exception); - } - else { - this.logger.logIfEnabled(FQCN, level, null, text); - } - } - else { - this.logger.logIfEnabled(FQCN, level, null, message, exception); - } - } - - protected Object readResolve() { - return new Log4jLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class Slf4jLog implements Log, Serializable { - - protected final String name; - - protected final transient T logger; - - public Slf4jLog(T logger) { - this.name = logger.getName(); - this.logger = logger; - } - - @Override - public boolean isFatalEnabled() { - return isErrorEnabled(); - } - - @Override - public boolean isErrorEnabled() { - return this.logger.isErrorEnabled(); - } - - @Override - public boolean isWarnEnabled() { - return this.logger.isWarnEnabled(); - } - - @Override - public boolean isInfoEnabled() { - return this.logger.isInfoEnabled(); - } - - @Override - public boolean isDebugEnabled() { - return this.logger.isDebugEnabled(); - } - - @Override - public boolean isTraceEnabled() { - return this.logger.isTraceEnabled(); - } - - @Override - public void fatal(Object message) { - error(message); - } - - @Override - public void fatal(Object message, Throwable exception) { - error(message, exception); - } - - @Override - public void error(Object message) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.error(String.valueOf(message)); - } - } - - @Override - public void error(Object message, Throwable exception) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.error(String.valueOf(message), exception); - } - } - - @Override - public void warn(Object message) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.warn(String.valueOf(message)); - } - } - - @Override - public void warn(Object message, Throwable exception) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.warn(String.valueOf(message), exception); - } - } - - @Override - public void info(Object message) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.info(String.valueOf(message)); - } - } - - @Override - public void info(Object message, Throwable exception) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.info(String.valueOf(message), exception); - } - } - - @Override - public void debug(Object message) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.debug(String.valueOf(message)); - } - } - - @Override - public void debug(Object message, Throwable exception) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.debug(String.valueOf(message), exception); - } - } - - @Override - public void trace(Object message) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.trace(String.valueOf(message)); - } - } - - @Override - public void trace(Object message, Throwable exception) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.trace(String.valueOf(message), exception); - } - } - - protected Object readResolve() { - return Slf4jAdapter.createLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class Slf4jLocationAwareLog extends Slf4jLog implements Serializable { - - private static final String FQCN = Slf4jLocationAwareLog.class.getName(); - - public Slf4jLocationAwareLog(LocationAwareLogger logger) { - super(logger); - } - - @Override - public void fatal(Object message) { - error(message); - } - - @Override - public void fatal(Object message, Throwable exception) { - error(message, exception); - } - - @Override - public void error(Object message) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, String.valueOf(message), null, null); - } - } - - @Override - public void error(Object message, Throwable exception) { - if (message instanceof String || this.logger.isErrorEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.ERROR_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void warn(Object message) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.WARN_INT, String.valueOf(message), null, null); - } - } - - @Override - public void warn(Object message, Throwable exception) { - if (message instanceof String || this.logger.isWarnEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.WARN_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void info(Object message) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, null); - } - } - - @Override - public void info(Object message, Throwable exception) { - if (message instanceof String || this.logger.isInfoEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.INFO_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void debug(Object message) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, String.valueOf(message), null, null); - } - } - - @Override - public void debug(Object message, Throwable exception) { - if (message instanceof String || this.logger.isDebugEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.DEBUG_INT, String.valueOf(message), null, exception); - } - } - - @Override - public void trace(Object message) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, String.valueOf(message), null, null); - } - } - - @Override - public void trace(Object message, Throwable exception) { - if (message instanceof String || this.logger.isTraceEnabled()) { - this.logger.log(null, FQCN, LocationAwareLogger.TRACE_INT, String.valueOf(message), null, exception); - } - } - - @Override - protected Object readResolve() { - return Slf4jAdapter.createLocationAwareLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class JavaUtilLog implements Log, Serializable { - - private final String name; - - private final transient java.util.logging.Logger logger; - - public JavaUtilLog(String name) { - this.name = name; - this.logger = java.util.logging.Logger.getLogger(name); - } - - @Override - public boolean isFatalEnabled() { - return isErrorEnabled(); - } - - @Override - public boolean isErrorEnabled() { - return this.logger.isLoggable(java.util.logging.Level.SEVERE); - } - - @Override - public boolean isWarnEnabled() { - return this.logger.isLoggable(java.util.logging.Level.WARNING); - } - - @Override - public boolean isInfoEnabled() { - return this.logger.isLoggable(java.util.logging.Level.INFO); - } - - @Override - public boolean isDebugEnabled() { - return this.logger.isLoggable(java.util.logging.Level.FINE); - } - - @Override - public boolean isTraceEnabled() { - return this.logger.isLoggable(java.util.logging.Level.FINEST); - } - - @Override - public void fatal(Object message) { - error(message); - } - - @Override - public void fatal(Object message, Throwable exception) { - error(message, exception); - } - - @Override - public void error(Object message) { - log(java.util.logging.Level.SEVERE, message, null); - } - - @Override - public void error(Object message, Throwable exception) { - log(java.util.logging.Level.SEVERE, message, exception); - } - - @Override - public void warn(Object message) { - log(java.util.logging.Level.WARNING, message, null); - } - - @Override - public void warn(Object message, Throwable exception) { - log(java.util.logging.Level.WARNING, message, exception); - } - - @Override - public void info(Object message) { - log(java.util.logging.Level.INFO, message, null); - } - - @Override - public void info(Object message, Throwable exception) { - log(java.util.logging.Level.INFO, message, exception); - } - - @Override - public void debug(Object message) { - log(java.util.logging.Level.FINE, message, null); - } - - @Override - public void debug(Object message, Throwable exception) { - log(java.util.logging.Level.FINE, message, exception); - } - - @Override - public void trace(Object message) { - log(java.util.logging.Level.FINEST, message, null); - } - - @Override - public void trace(Object message, Throwable exception) { - log(java.util.logging.Level.FINEST, message, exception); - } - - private void log(java.util.logging.Level level, Object message, Throwable exception) { - if (this.logger.isLoggable(level)) { - LogRecord rec; - if (message instanceof LogRecord logRecord) { - rec = logRecord; - } - else { - rec = new LocationResolvingLogRecord(level, String.valueOf(message)); - rec.setLoggerName(this.name); - rec.setResourceBundleName(this.logger.getResourceBundleName()); - rec.setResourceBundle(this.logger.getResourceBundle()); - rec.setThrown(exception); - } - logger.log(rec); - } - } - - protected Object readResolve() { - return new JavaUtilLog(this.name); - } - } - - - @SuppressWarnings("serial") - private static class LocationResolvingLogRecord extends LogRecord { - - private static final String FQCN = JavaUtilLog.class.getName(); - - private volatile boolean resolved; - - public LocationResolvingLogRecord(java.util.logging.Level level, String msg) { - super(level, msg); - } - - @Override - public String getSourceClassName() { - if (!this.resolved) { - resolve(); - } - return super.getSourceClassName(); - } - - @Override - public void setSourceClassName(String sourceClassName) { - super.setSourceClassName(sourceClassName); - this.resolved = true; - } - - @Override - public String getSourceMethodName() { - if (!this.resolved) { - resolve(); - } - return super.getSourceMethodName(); - } - - @Override - public void setSourceMethodName(String sourceMethodName) { - super.setSourceMethodName(sourceMethodName); - this.resolved = true; - } - - private void resolve() { - StackTraceElement[] stack = new Throwable().getStackTrace(); - String sourceClassName = null; - String sourceMethodName = null; - boolean found = false; - for (StackTraceElement element : stack) { - String className = element.getClassName(); - if (FQCN.equals(className)) { - found = true; - } - else if (found) { - sourceClassName = className; - sourceMethodName = element.getMethodName(); - break; - } - } - setSourceClassName(sourceClassName); - setSourceMethodName(sourceMethodName); - } - - protected Object writeReplace() { - LogRecord serialized = new LogRecord(getLevel(), getMessage()); - serialized.setLoggerName(getLoggerName()); - serialized.setResourceBundle(getResourceBundle()); - serialized.setResourceBundleName(getResourceBundleName()); - serialized.setSourceClassName(getSourceClassName()); - serialized.setSourceMethodName(getSourceMethodName()); - serialized.setSequenceNumber(getSequenceNumber()); - serialized.setParameters(getParameters()); - serialized.setLongThreadID(getLongThreadID()); - serialized.setInstant(getInstant()); - serialized.setThrown(getThrown()); - return serialized; - } - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java deleted file mode 100644 index 095cdbb9b629..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactory.java +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.apache.commons.logging; - -/** - * A minimal incarnation of Apache Commons Logging's {@code LogFactory} API, - * providing just the common {@link Log} lookup methods. This is inspired - * by the JCL-over-SLF4J bridge and should be source as well as binary - * compatible with all common use of the Commons Logging API (in particular: - * with {@code LogFactory.getLog(Class/String)} field initializers). - * - *

    This implementation does not support Commons Logging's original provider - * detection. It rather only checks for the presence of the Log4j 2.x API - * and the SLF4J 1.7 API in the Spring Framework classpath, falling back to - * {@code java.util.logging} if none of the two is available. In that sense, - * it works as a replacement for the Log4j 2 Commons Logging bridge as well as - * the JCL-over-SLF4J bridge, both of which become irrelevant for Spring-based - * setups as a consequence (with no need for manual excludes of the standard - * Commons Logging API jar anymore either). Furthermore, for simple setups - * without an external logging provider, Spring does not require any extra jar - * on the classpath anymore since this embedded log factory automatically - * delegates to {@code java.util.logging} in such a scenario. - * - *

    Note that this Commons Logging variant is only meant to be used for - * infrastructure logging purposes in the core framework and in extensions. - * It also serves as a common bridge for third-party libraries using the - * Commons Logging API, for example, Apache HttpClient, and HtmlUnit, bringing - * them into the same consistent arrangement without any extra bridge jars. - * - *

    For logging need in application code, prefer direct use of Log4j 2.x - * or SLF4J or {@code java.util.logging}. Simply put Log4j 2.x or Logback - * (or another SLF4J provider) onto your classpath, without any extra bridges, - * and let the framework auto-adapt to your choice. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - */ -public abstract class LogFactory { - - /** - * Convenience method to return a named logger. - * @param clazz containing Class from which a log name will be derived - */ - public static Log getLog(Class clazz) { - return getLog(clazz.getName()); - } - - /** - * Convenience method to return a named logger. - * @param name logical name of the Log instance to be returned - */ - public static Log getLog(String name) { - return LogAdapter.createLog(name); - } - - - /** - * This method only exists for compatibility with unusual Commons Logging API - * usage like, for example, {@code LogFactory.getFactory().getInstance(Class/String)}. - * @see #getInstance(Class) - * @see #getInstance(String) - * @deprecated in favor of {@link #getLog(Class)}/{@link #getLog(String)} - */ - @Deprecated - public static LogFactory getFactory() { - return new LogFactory() { - @Override - public Object getAttribute(String name) { - return null; - } - @Override - public String[] getAttributeNames() { - return new String[0]; - } - @Override - public void removeAttribute(String name) { - } - @Override - public void setAttribute(String name, Object value) { - } - @Override - public void release() { - } - }; - } - - /** - * Convenience method to return a named logger. - *

    This variant just dispatches straight to {@link #getLog(Class)}. - * @param clazz containing Class from which a log name will be derived - * @deprecated in favor of {@link #getLog(Class)} - */ - @Deprecated - public Log getInstance(Class clazz) { - return getLog(clazz); - } - - /** - * Convenience method to return a named logger. - *

    This variant just dispatches straight to {@link #getLog(String)}. - * @param name logical name of the Log instance to be returned - * @deprecated in favor of {@link #getLog(String)} - */ - @Deprecated - public Log getInstance(String name) { - return getLog(name); - } - - - // Just in case some code happens to call uncommon Commons Logging methods... - - @Deprecated - public abstract Object getAttribute(String name); - - @Deprecated - public abstract String[] getAttributeNames(); - - @Deprecated - public abstract void removeAttribute(String name); - - @Deprecated - public abstract void setAttribute(String name, Object value); - - @Deprecated - public abstract void release(); - - @Deprecated - public static void release(ClassLoader classLoader) { - // do nothing - } - - @Deprecated - public static void releaseAll() { - // do nothing - } - - @Deprecated - public static String objectId(Object o) { - return (o == null ? "null" : o.getClass().getName() + "@" + System.identityHashCode(o)); - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java b/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java deleted file mode 100644 index 355d4a30bce4..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/LogFactoryService.java +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.apache.commons.logging; - -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; - -/** - * A minimal subclass of the standard Apache Commons Logging's {@code LogFactory} class, - * overriding the abstract {@code getInstance} lookup methods. This is just applied in - * case of the standard {@code commons-logging} jar accidentally ending up on the classpath, - * with the standard {@code LogFactory} class performing its META-INF service discovery. - * This implementation simply delegates to Spring's common {@link Log} factory methods. - * - * @author Juergen Hoeller - * @since 5.1 - * @deprecated since it is only meant to be used in the above-mentioned fallback scenario - */ -@Deprecated -public class LogFactoryService extends LogFactory { - - private final Map attributes = new ConcurrentHashMap<>(); - - - public LogFactoryService() { - System.out.println("Standard Commons Logging discovery in action with spring-jcl: " + - "please remove commons-logging.jar from classpath in order to avoid potential conflicts"); - } - - - @Override - public Log getInstance(Class clazz) { - return getInstance(clazz.getName()); - } - - @Override - public Log getInstance(String name) { - return LogAdapter.createLog(name); - } - - - // Just in case some code happens to rely on Commons Logging attributes... - - @Override - public void setAttribute(String name, Object value) { - if (value != null) { - this.attributes.put(name, value); - } - else { - this.attributes.remove(name); - } - } - - @Override - public void removeAttribute(String name) { - this.attributes.remove(name); - } - - @Override - public Object getAttribute(String name) { - return this.attributes.get(name); - } - - @Override - public String[] getAttributeNames() { - return this.attributes.keySet().toArray(new String[0]); - } - - @Override - public void release() { - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java deleted file mode 100644 index 5c0361d77394..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/impl/NoOpLog.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright 2002-2017 the original author 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.apache.commons.logging.impl; - -import java.io.Serializable; - -import org.apache.commons.logging.Log; - -/** - * Trivial implementation of {@link Log} that throws away all messages. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - */ -@SuppressWarnings("serial") -public class NoOpLog implements Log, Serializable { - - public NoOpLog() { - } - - public NoOpLog(String name) { - } - - - @Override - public boolean isFatalEnabled() { - return false; - } - - @Override - public boolean isErrorEnabled() { - return false; - } - - @Override - public boolean isWarnEnabled() { - return false; - } - - @Override - public boolean isInfoEnabled() { - return false; - } - - @Override - public boolean isDebugEnabled() { - return false; - } - - @Override - public boolean isTraceEnabled() { - return false; - } - - @Override - public void fatal(Object message) { - } - - @Override - public void fatal(Object message, Throwable t) { - } - - @Override - public void error(Object message) { - } - - @Override - public void error(Object message, Throwable t) { - } - - @Override - public void warn(Object message) { - } - - @Override - public void warn(Object message, Throwable t) { - } - - @Override - public void info(Object message) { - } - - @Override - public void info(Object message, Throwable t) { - } - - @Override - public void debug(Object message) { - } - - @Override - public void debug(Object message, Throwable t) { - } - - @Override - public void trace(Object message) { - } - - @Override - public void trace(Object message, Throwable t) { - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java deleted file mode 100644 index 3dbcbaf16b2e..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/impl/SimpleLog.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2002-2018 the original author 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.apache.commons.logging.impl; - -/** - * Originally a simple Commons Logging provider configured by system properties. - * Deprecated in {@code spring-jcl}, effectively equivalent to {@link NoOpLog}. - * - *

    Instead of instantiating this directly, call {@code LogFactory#getLog(Class/String)} - * which will fall back to {@code java.util.logging} if neither Log4j nor SLF4J are present. - * - * @author Juergen Hoeller (for the {@code spring-jcl} variant) - * @since 5.0 - * @deprecated in {@code spring-jcl} (effectively equivalent to {@link NoOpLog}) - */ -@Deprecated -@SuppressWarnings("serial") -public class SimpleLog extends NoOpLog { - - public SimpleLog(String name) { - super(name); - System.out.println(SimpleLog.class.getName() + " is deprecated and equivalent to NoOpLog in spring-jcl. " + - "Use a standard LogFactory.getLog(Class/String) call instead."); - } - -} diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java b/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java deleted file mode 100644 index fe9899ca2b33..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/impl/package-info.java +++ /dev/null @@ -1,11 +0,0 @@ -/** - * Spring's variant of the - * Commons Logging API: - * with special support for Log4J 2, SLF4J and {@code java.util.logging}. - * - *

    This {@code impl} package is only present for binary compatibility - * with existing Commons Logging usage, for example, in Commons Configuration. - * {@code NoOpLog} can be used as a {@code Log} fallback instance, and - * {@code SimpleLog} is not meant to work (issuing a warning when used). - */ -package org.apache.commons.logging.impl; diff --git a/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java b/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java deleted file mode 100644 index cbf63edfff63..000000000000 --- a/spring-jcl/src/main/java/org/apache/commons/logging/package-info.java +++ /dev/null @@ -1,25 +0,0 @@ -/** - * Spring's variant of the - * Commons Logging API: - * with special support for Log4J 2, SLF4J and {@code java.util.logging}. - * - *

    This is a custom bridge along the lines of {@code jcl-over-slf4j}. - * You may exclude {@code spring-jcl} and switch to {@code jcl-over-slf4j} - * instead if you prefer the hard-bound SLF4J bridge. However, Spring's own - * bridge provides a better out-of-the-box experience when using Log4J 2 - * or {@code java.util.logging}, with no extra bridge jars necessary, and - * also easier setup of SLF4J with Logback (no JCL exclude, no JCL bridge). - * - *

    {@link org.apache.commons.logging.Log} is equivalent to the original. - * However, {@link org.apache.commons.logging.LogFactory} is a very different - * implementation which is minimized and optimized for Spring's purposes, - * detecting Log4J 2.x and SLF4J 1.7 in the framework classpath and falling - * back to {@code java.util.logging}. If you run into any issues with this - * implementation, consider excluding {@code spring-jcl} and switching to the - * standard {@code commons-logging} artifact or to {@code jcl-over-slf4j}. - * - *

    Note that this Commons Logging bridge is only meant to be used for - * framework logging purposes, both in the core framework and in extensions. - * For applications, prefer direct use of Log4J/SLF4J or {@code java.util.logging}. - */ -package org.apache.commons.logging; diff --git a/spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory b/spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory deleted file mode 100644 index 375a5aa79f9c..000000000000 --- a/spring-jcl/src/main/resources/META-INF/services/org.apache.commons.logging.LogFactory +++ /dev/null @@ -1 +0,0 @@ -org.apache.commons.logging.LogFactoryService \ No newline at end of file diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java index eb0eb0f184e5..6fa6d3ef3fcf 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/BadSqlGrammarException.java @@ -18,8 +18,9 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.lang.Nullable; /** * Exception thrown when SQL specified is invalid. Such exceptions always have @@ -53,8 +54,7 @@ public BadSqlGrammarException(String task, String sql, SQLException ex) { /** * Return the wrapped SQLException. */ - @Nullable - public SQLException getSQLException() { + public @Nullable SQLException getSQLException() { return (SQLException) getCause(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java index 1439b89db13c..4cf6fd381669 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/CannotGetJdbcConnectionException.java @@ -18,8 +18,9 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; /** * Fatal exception thrown when we can't connect to an RDBMS using JDBC. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java index d671af15f7a6..9b76ad5a8d8b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/InvalidResultSetAccessException.java @@ -18,8 +18,9 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.lang.Nullable; /** * Exception thrown when a ResultSet has been accessed in an invalid fashion. @@ -36,8 +37,7 @@ @SuppressWarnings("serial") public class InvalidResultSetAccessException extends InvalidDataAccessResourceUsageException { - @Nullable - private final String sql; + private final @Nullable String sql; /** @@ -64,8 +64,7 @@ public InvalidResultSetAccessException(SQLException ex) { /** * Return the wrapped SQLException. */ - @Nullable - public SQLException getSQLException() { + public @Nullable SQLException getSQLException() { return (SQLException) getCause(); } @@ -73,8 +72,7 @@ public SQLException getSQLException() { * Return the SQL that caused the problem. * @return the offending SQL, if known */ - @Nullable - public String getSql() { + public @Nullable String getSql() { return this.sql; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java index 2019b2ebd2f9..7acc07e8f5f5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/SQLWarningException.java @@ -18,8 +18,9 @@ import java.sql.SQLWarning; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; /** * Exception thrown when we're not ignoring {@link java.sql.SQLWarning SQLWarnings}. @@ -50,8 +51,7 @@ public SQLWarningException(String msg, SQLWarning ex) { * Return the underlying {@link SQLWarning}. * @since 5.3.29 */ - @Nullable - public SQLWarning getSQLWarning() { + public @Nullable SQLWarning getSQLWarning() { return (SQLWarning) getCause(); } @@ -60,8 +60,7 @@ public SQLWarning getSQLWarning() { * @deprecated as of 5.3.29, in favor of {@link #getSQLWarning()} */ @Deprecated(since = "5.3.29") - @Nullable - public SQLWarning SQLWarning() { + public @Nullable SQLWarning SQLWarning() { return getSQLWarning(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java index 897acfbdbdb6..e64155f7d834 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/UncategorizedSQLException.java @@ -18,8 +18,9 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; /** * Exception thrown when we can't classify an SQLException into @@ -32,8 +33,7 @@ public class UncategorizedSQLException extends UncategorizedDataAccessException { /** SQL that led to the problem. */ - @Nullable - private final String sql; + private final @Nullable String sql; /** @@ -53,16 +53,14 @@ public UncategorizedSQLException(String task, @Nullable String sql, SQLException /** * Return the underlying SQLException. */ - @Nullable - public SQLException getSQLException() { + public @Nullable SQLException getSQLException() { return (SQLException) getCause(); } /** * Return the SQL that led to the problem (if known). */ - @Nullable - public String getSql() { + public @Nullable String getSql() { return this.sql; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java index 08d07565bc97..cd6e09e94f89 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/config/DatabasePopulatorConfigUtils.java @@ -18,6 +18,7 @@ import java.util.List; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.BeanMetadataElement; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.support.ManagedList; import org.springframework.jdbc.datasource.init.CompositeDatabasePopulator; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import org.springframework.util.xml.DomUtils; @@ -48,8 +48,7 @@ public static void setDatabasePopulator(Element element, BeanDefinitionBuilder b } } - @Nullable - private static BeanDefinition createDatabasePopulator(Element element, List scripts, String execution) { + private static @Nullable BeanDefinition createDatabasePopulator(Element element, List scripts, String execution) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(CompositeDatabasePopulator.class); boolean ignoreFailedDrops = element.getAttribute("ignore-failures").equals("DROPS"); @@ -90,8 +89,7 @@ private static BeanDefinition createDatabasePopulator(Element element, List implements RowMapper { protected final Log logger = LogFactory.getLog(getClass()); /** The class we are mapping to. */ - @Nullable - private Class mappedClass; + private @Nullable Class mappedClass; /** Whether we're strictly validating. */ private boolean checkFullyPopulated = false; @@ -109,16 +108,13 @@ public class BeanPropertyRowMapper implements RowMapper { private boolean primitivesDefaultedForNullValue = false; /** ConversionService for binding JDBC values to bean properties. */ - @Nullable - private ConversionService conversionService = DefaultConversionService.getSharedInstance(); + private @Nullable ConversionService conversionService = DefaultConversionService.getSharedInstance(); /** Map of the properties we provide mapping for. */ - @Nullable - private Map mappedProperties; + private @Nullable Map mappedProperties; /** Set of bean property names we provide mapping for. */ - @Nullable - private Set mappedPropertyNames; + private @Nullable Set mappedPropertyNames; /** @@ -168,8 +164,7 @@ public void setMappedClass(Class mappedClass) { /** * Get the class that we are mapping to. */ - @Nullable - public final Class getMappedClass() { + public final @Nullable Class getMappedClass() { return this.mappedClass; } @@ -233,8 +228,7 @@ public void setConversionService(@Nullable ConversionService conversionService) * or {@code null} if none. * @since 4.3 */ - @Nullable - public ConversionService getConversionService() { + public @Nullable ConversionService getConversionService() { return this.conversionService; } @@ -267,7 +261,7 @@ protected void initialize(Class mappedClass) { * @param propertyName the property name (as used by property descriptors) * @since 5.3.9 */ - protected void suppressProperty(String propertyName) { + protected void suppressProperty(@Nullable String propertyName) { if (this.mappedProperties != null) { this.mappedProperties.remove(lowerCaseName(propertyName)); this.mappedProperties.remove(underscoreName(propertyName)); @@ -302,7 +296,10 @@ protected Set mappedNames(PropertyDescriptor pd) { * @since 4.2 * @see #underscoreName */ - protected String lowerCaseName(String name) { + protected String lowerCaseName(@Nullable String name) { + if (!StringUtils.hasLength(name)) { + return ""; + } return name.toLowerCase(Locale.US); } @@ -314,7 +311,7 @@ protected String lowerCaseName(String name) { * @since 4.2 * @see JdbcUtils#convertPropertyNameToUnderscoreName */ - protected String underscoreName(String name) { + protected String underscoreName(@Nullable String name) { return JdbcUtils.convertPropertyNameToUnderscoreName(name); } @@ -426,8 +423,7 @@ protected void initBeanWrapper(BeanWrapper bw) { * @throws SQLException in case of extraction failure * @see #getColumnValue(ResultSet, int, Class) */ - @Nullable - protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { + protected @Nullable Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) throws SQLException { return JdbcUtils.getResultSetValue(rs, index, pd.getPropertyType()); } @@ -445,8 +441,7 @@ protected Object getColumnValue(ResultSet rs, int index, PropertyDescriptor pd) * @since 5.3 * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) */ - @Nullable - protected Object getColumnValue(ResultSet rs, int index, Class paramType) throws SQLException { + protected @Nullable Object getColumnValue(ResultSet rs, int index, Class paramType) throws SQLException { return JdbcUtils.getResultSetValue(rs, index, paramType); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java index 7fb2ad7f0aca..dc929ea87371 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCallback.java @@ -19,8 +19,9 @@ import java.sql.CallableStatement; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Generic callback interface for code that operates on a CallableStatement. @@ -74,7 +75,6 @@ public interface CallableStatementCallback { * into a DataAccessException by an SQLExceptionTranslator * @throws DataAccessException in case of custom exceptions */ - @Nullable - T doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException; + @Nullable T doInCallableStatement(CallableStatement cs) throws SQLException, DataAccessException; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java index 501938d45e18..03208b18b743 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/CallableStatementCreatorFactory.java @@ -25,8 +25,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; /** * Helper class that efficiently creates multiple {@link CallableStatementCreator} @@ -130,11 +131,9 @@ public CallableStatementCreator newCallableStatementCreator(ParameterMapper inPa */ private class CallableStatementCreatorImpl implements CallableStatementCreator, SqlProvider, ParameterDisposer { - @Nullable - private ParameterMapper inParameterMapper; + private @Nullable ParameterMapper inParameterMapper; - @Nullable - private Map inParameters; + private @Nullable Map inParameters; /** * Create a new CallableStatementCreatorImpl. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java index e6ea97a98c41..f310823c7428 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ColumnMapRowMapper.java @@ -21,8 +21,9 @@ import java.sql.SQLException; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedCaseInsensitiveMap; /** @@ -90,8 +91,7 @@ protected String getColumnKey(String columnName) { * @return the Object returned * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue */ - @Nullable - protected Object getColumnValue(ResultSet rs, int index) throws SQLException { + protected @Nullable Object getColumnValue(ResultSet rs, int index) throws SQLException { return JdbcUtils.getResultSetValue(rs, index); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java index 25bbfd49499d..19eac368c464 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ConnectionCallback.java @@ -19,8 +19,9 @@ import java.sql.Connection; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Generic callback interface for code that operates on a JDBC Connection. @@ -64,7 +65,6 @@ public interface ConnectionCallback { * @see JdbcTemplate#queryForObject(String, Class) * @see JdbcTemplate#queryForRowSet(String) */ - @Nullable - T doInConnection(Connection con) throws SQLException, DataAccessException; + @Nullable T doInConnection(Connection con) throws SQLException, DataAccessException; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java index a8ba50dae0ab..1cdb14a4e44f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/DataClassRowMapper.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,12 +20,13 @@ import java.sql.ResultSet; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -61,14 +62,11 @@ */ public class DataClassRowMapper extends BeanPropertyRowMapper { - @Nullable - private Constructor mappedConstructor; + private @Nullable Constructor mappedConstructor; - @Nullable - private String[] constructorParameterNames; + private @Nullable String @Nullable [] constructorParameterNames; - @Nullable - private TypeDescriptor[] constructorParameterTypes; + private TypeDescriptor @Nullable [] constructorParameterTypes; /** @@ -110,7 +108,7 @@ protected void initialize(Class mappedClass) { protected T constructMappedInstance(ResultSet rs, TypeConverter tc) throws SQLException { Assert.state(this.mappedConstructor != null, "Mapped constructor was not initialized"); - Object[] args; + @Nullable Object[] args; if (this.constructorParameterNames != null && this.constructorParameterTypes != null) { args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { 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 9824337edcf7..b7423196ea99 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. @@ -21,10 +21,11 @@ import java.util.Map; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.lang.Nullable; /** * Interface specifying a basic set of JDBC operations. @@ -67,8 +68,7 @@ public interface JdbcOperations { * @return a result object returned by the action, or {@code null} if none * @throws DataAccessException if there is any problem */ - @Nullable - T execute(ConnectionCallback action) throws DataAccessException; + @Nullable T execute(ConnectionCallback action) throws DataAccessException; //------------------------------------------------------------------------- @@ -87,8 +87,7 @@ public interface JdbcOperations { * @return a result object returned by the action, or {@code null} if none * @throws DataAccessException if there is any problem */ - @Nullable - T execute(StatementCallback action) throws DataAccessException; + @Nullable T execute(StatementCallback action) throws DataAccessException; /** * Issue a single SQL execute, typically a DDL statement. @@ -109,8 +108,7 @@ public interface JdbcOperations { * @throws DataAccessException if there is any problem executing the query * @see #query(String, ResultSetExtractor, Object...) */ - @Nullable - T query(String sql, ResultSetExtractor rse) throws DataAccessException; + @Nullable T query(String sql, ResultSetExtractor rse) throws DataAccessException; /** * Execute a query given static SQL, reading the ResultSet on a per-row @@ -165,14 +163,13 @@ public interface JdbcOperations { * @param sql the SQL query to execute * @param rowMapper a callback that will map one object per row * @return the single mapped object (may be {@code null} if the given - * {@link RowMapper} returned {@code} null) + * {@link RowMapper} returned {@code null}) * @throws org.springframework.dao.IncorrectResultSizeDataAccessException * if the query does not return exactly one row * @throws DataAccessException if there is any problem executing the query * @see #queryForObject(String, RowMapper, Object...) */ - @Nullable - T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException; + @Nullable T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException; /** * Execute a query for a result object, given static SQL. @@ -193,8 +190,7 @@ public interface JdbcOperations { * @throws DataAccessException if there is any problem executing the query * @see #queryForObject(String, Class, Object...) */ - @Nullable - T queryForObject(String sql, Class requiredType) throws DataAccessException; + @Nullable T queryForObject(String sql, Class requiredType) throws DataAccessException; /** * Execute a query for a result map, given static SQL. @@ -303,8 +299,7 @@ public interface JdbcOperations { * @return a result object returned by the action, or {@code null} if none * @throws DataAccessException if there is any problem */ - @Nullable - T execute(PreparedStatementCreator psc, PreparedStatementCallback action) throws DataAccessException; + @Nullable T execute(PreparedStatementCreator psc, PreparedStatementCallback action) throws DataAccessException; /** * Execute a JDBC data access operation, implemented as callback action @@ -319,8 +314,7 @@ public interface JdbcOperations { * @return a result object returned by the action, or {@code null} if none * @throws DataAccessException if there is any problem */ - @Nullable - T execute(String sql, PreparedStatementCallback action) throws DataAccessException; + @Nullable T execute(String sql, PreparedStatementCallback action) throws DataAccessException; /** * Query using a prepared statement, reading the ResultSet with a ResultSetExtractor. @@ -332,8 +326,7 @@ public interface JdbcOperations { * @throws DataAccessException if there is any problem * @see PreparedStatementCreatorFactory */ - @Nullable - T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException; + @Nullable T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException; /** * Query using a prepared statement, reading the ResultSet with a ResultSetExtractor. @@ -346,8 +339,7 @@ public interface JdbcOperations { * @return an arbitrary result object, as returned by the ResultSetExtractor * @throws DataAccessException if there is any problem */ - @Nullable - T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) + @Nullable T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException; /** @@ -362,8 +354,7 @@ T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtracto * @throws DataAccessException if the query fails * @see java.sql.Types */ - @Nullable - T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException; + @Nullable T query(String sql, @Nullable Object @Nullable [] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of arguments @@ -376,11 +367,10 @@ T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtracto * @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 - @Nullable - T query(String sql, @Nullable Object[] args, ResultSetExtractor rse) throws DataAccessException; + @Deprecated(since = "5.3") + @Nullable T query(String sql, @Nullable Object @Nullable [] args, ResultSetExtractor rse) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of arguments @@ -395,8 +385,7 @@ T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtracto * @throws DataAccessException if the query fails * @since 3.0.1 */ - @Nullable - T query(String sql, ResultSetExtractor rse, @Nullable Object... args) throws DataAccessException; + @Nullable T query(String sql, ResultSetExtractor rse, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query using a prepared statement, reading the ResultSet on a per-row basis @@ -436,7 +425,7 @@ T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtracto * @throws DataAccessException if the query fails * @see java.sql.Types */ - void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException; + void query(String sql, @Nullable Object @Nullable [] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -449,10 +438,10 @@ T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtracto * 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 - void query(String sql, @Nullable Object[] args, RowCallbackHandler rch) throws DataAccessException; + @Deprecated(since = "5.3") + void query(String sql, @Nullable Object @Nullable [] args, RowCallbackHandler rch) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -467,7 +456,7 @@ T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtracto * @throws DataAccessException if the query fails * @since 3.0.1 */ - void query(String sql, RowCallbackHandler rch, @Nullable Object... args) throws DataAccessException; + void query(String sql, RowCallbackHandler rch, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query using a prepared statement, mapping each row to a result object @@ -511,7 +500,7 @@ List query(String sql, @Nullable PreparedStatementSetter pss, RowMapper List query(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException; + List query(String sql, @Nullable Object @Nullable [] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -525,10 +514,10 @@ List query(String sql, @Nullable PreparedStatementSetter pss, RowMapper List query(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException; + @Deprecated(since = "5.3") + List query(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -544,7 +533,7 @@ List query(String sql, @Nullable PreparedStatementSetter pss, RowMapper List query(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException; + List query(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query using a prepared statement, mapping each row to a result object @@ -595,7 +584,7 @@ Stream queryForStream(String sql, @Nullable PreparedStatementSetter pss, * @throws DataAccessException if the query fails * @since 5.3 */ - Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Object... args) + Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException; /** @@ -609,13 +598,12 @@ Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Objec * (constants from {@code java.sql.Types}) * @param rowMapper a callback that will map one object per row * @return the single mapped object (may be {@code null} if the given - * {@link RowMapper} returned {@code} null) + * {@link RowMapper} returned {@code null}) * @throws org.springframework.dao.IncorrectResultSizeDataAccessException * if the query does not return exactly one row * @throws DataAccessException if the query fails */ - @Nullable - T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException; /** @@ -629,15 +617,14 @@ T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper row * only the argument value but also the SQL type and optionally the scale * @param rowMapper a callback that will map one object per row * @return the single mapped object (may be {@code null} if the given - * {@link RowMapper} returned {@code} null) + * {@link RowMapper} returned {@code null}) * @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 - @Nullable - T queryForObject(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException; + @Deprecated(since = "5.3") + @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list @@ -650,14 +637,13 @@ T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper row * may also contain {@link SqlParameterValue} objects which indicate not * only the argument value but also the SQL type and optionally the scale * @return the single mapped object (may be {@code null} if the given - * {@link RowMapper} returned {@code} null) + * {@link RowMapper} returned {@code null}) * @throws org.springframework.dao.IncorrectResultSizeDataAccessException * if the query does not return exactly one row * @throws DataAccessException if the query fails * @since 3.0.1 */ - @Nullable - T queryForObject(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException; + @Nullable T queryForObject(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -678,8 +664,7 @@ T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper row * @see #queryForObject(String, Class) * @see java.sql.Types */ - @Nullable - T queryForObject(String sql, Object[] args, int[] argTypes, Class requiredType) + @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, int[] argTypes, Class requiredType) throws DataAccessException; /** @@ -700,11 +685,10 @@ T queryForObject(String sql, Object[] args, int[] argTypes, Class require * 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 - @Nullable - T queryForObject(String sql, @Nullable Object[] args, Class requiredType) throws DataAccessException; + @Deprecated(since = "5.3") + @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, Class requiredType) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -726,8 +710,7 @@ T queryForObject(String sql, Object[] args, int[] argTypes, Class require * @since 3.0.1 * @see #queryForObject(String, Class) */ - @Nullable - T queryForObject(String sql, Class requiredType, @Nullable Object... args) throws DataAccessException; + @Nullable T queryForObject(String sql, Class requiredType, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -746,7 +729,7 @@ T queryForObject(String sql, Object[] args, int[] argTypes, Class require * @see ColumnMapRowMapper * @see java.sql.Types */ - Map queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException; + Map queryForMap(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -769,7 +752,7 @@ T queryForObject(String sql, Object[] args, int[] argTypes, Class require * @see #queryForMap(String) * @see ColumnMapRowMapper */ - Map queryForMap(String sql, @Nullable Object... args) throws DataAccessException; + Map queryForMap(String sql, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -787,7 +770,7 @@ T queryForObject(String sql, Object[] args, int[] argTypes, Class require * @see #queryForList(String, Class) * @see SingleColumnRowMapper */ - List queryForList(String sql, Object[] args, int[] argTypes, Class elementType) + List queryForList(String sql, @Nullable Object @Nullable [] args, int[] argTypes, Class elementType) throws DataAccessException; /** @@ -806,10 +789,10 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @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 - List queryForList(String sql, @Nullable Object[] args, Class elementType) throws DataAccessException; + @Deprecated(since = "5.3") + List queryForList(String sql, @Nullable Object @Nullable [] args, Class elementType) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -829,7 +812,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @see #queryForList(String, Class) * @see SingleColumnRowMapper */ - List queryForList(String sql, Class elementType, @Nullable Object... args) throws DataAccessException; + List queryForList(String sql, Class elementType, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -847,7 +830,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @see #queryForList(String) * @see java.sql.Types */ - List> queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException; + List> queryForList(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -865,7 +848,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @throws DataAccessException if the query fails * @see #queryForList(String) */ - List> queryForList(String sql, @Nullable Object... args) throws DataAccessException; + List> queryForList(String sql, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -887,7 +870,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @see javax.sql.rowset.CachedRowSet * @see java.sql.Types */ - SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes) throws DataAccessException; + SqlRowSet queryForRowSet(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -909,7 +892,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @see SqlRowSetResultSetExtractor * @see javax.sql.rowset.CachedRowSet */ - SqlRowSet queryForRowSet(String sql, @Nullable Object... args) throws DataAccessException; + SqlRowSet queryForRowSet(String sql, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Issue a single SQL update operation (such as an insert, update or delete @@ -965,7 +948,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @throws DataAccessException if there is any problem issuing the update * @see java.sql.Types */ - int update(String sql, Object[] args, int[] argTypes) throws DataAccessException; + int update(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException; /** * Issue a single SQL update operation (such as an insert, update or delete statement) @@ -978,7 +961,7 @@ List queryForList(String sql, Object[] args, int[] argTypes, Class ele * @return the number of rows affected * @throws DataAccessException if there is any problem issuing the update */ - int update(String sql, @Nullable Object... args) throws DataAccessException; + int update(String sql, @Nullable Object @Nullable ... args) throws DataAccessException; /** * Issue multiple update statements on a single PreparedStatement, @@ -1081,8 +1064,7 @@ int[][] batchUpdate(String sql, Collection batchArgs, int batchSize, * @return a result object returned by the action, or {@code null} if none * @throws DataAccessException if there is any problem */ - @Nullable - T execute(CallableStatementCreator csc, CallableStatementCallback action) throws DataAccessException; + @Nullable T execute(CallableStatementCreator csc, CallableStatementCallback action) throws DataAccessException; /** * Execute a JDBC data access operation, implemented as callback action @@ -1097,8 +1079,7 @@ int[][] batchUpdate(String sql, Collection batchArgs, int batchSize, * @return a result object returned by the action, or {@code null} if none * @throws DataAccessException if there is any problem */ - @Nullable - T execute(String callString, CallableStatementCallback action) throws DataAccessException; + @Nullable T execute(String callString, CallableStatementCallback action) throws DataAccessException; /** * Execute an SQL call using a CallableStatementCreator to provide SQL and 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 9401a8feba7e..227f494da158 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 @@ -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,6 +41,8 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.support.DataAccessUtils; @@ -53,7 +55,6 @@ import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedCaseInsensitiveMap; @@ -335,8 +336,7 @@ public boolean isResultsMapCaseInsensitive() { //------------------------------------------------------------------------- @Override - @Nullable - public T execute(ConnectionCallback action) throws DataAccessException { + public @Nullable T execute(ConnectionCallback action) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Connection con = DataSourceUtils.getConnection(obtainDataSource()); @@ -381,8 +381,7 @@ protected Connection createConnectionProxy(Connection con) { // Methods dealing with static SQL (java.sql.Statement) //------------------------------------------------------------------------- - @Nullable - private T execute(StatementCallback action, boolean closeResources) throws DataAccessException { + private @Nullable T execute(StatementCallback action, boolean closeResources) throws DataAccessException { Assert.notNull(action, "Callback object must not be null"); Connection con = DataSourceUtils.getConnection(obtainDataSource()); @@ -416,13 +415,12 @@ private T execute(StatementCallback action, boolean closeResources) throw } @Override - @Nullable - public T execute(StatementCallback action) throws DataAccessException { + public @Nullable T execute(StatementCallback action) throws DataAccessException { return execute(action, true); } @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 + "]"); } @@ -430,8 +428,7 @@ public void execute(final String sql) throws DataAccessException { // Callback to execute the statement. class ExecuteStatementCallback implements StatementCallback, SqlProvider { @Override - @Nullable - public Object doInStatement(Statement stmt) throws SQLException { + public @Nullable Object doInStatement(Statement stmt) throws SQLException { stmt.execute(sql); return null; } @@ -445,8 +442,7 @@ public String getSql() { } @Override - @Nullable - public 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()) { @@ -456,8 +452,7 @@ public T query(final String sql, final ResultSetExtractor rse) throws Dat // Callback to execute the query. class QueryStatementCallback implements StatementCallback, SqlProvider { @Override - @Nullable - public T doInStatement(Statement stmt) throws SQLException { + public @Nullable T doInStatement(Statement stmt) throws SQLException { ResultSet rs = null; try { rs = stmt.executeQuery(sql); @@ -514,15 +509,13 @@ public Map queryForMap(String sql) throws DataAccessException { } @Override - @Nullable - public T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException { + public @Nullable T queryForObject(String sql, RowMapper rowMapper) throws DataAccessException { List results = query(sql, rowMapper); return DataAccessUtils.nullableSingleResult(results); } @Override - @Nullable - public T queryForObject(String sql, Class requiredType) throws DataAccessException { + public @Nullable T queryForObject(String sql, Class requiredType) throws DataAccessException { return queryForObject(sql, getSingleColumnRowMapper(requiredType)); } @@ -542,7 +535,7 @@ public SqlRowSet queryForRowSet(String sql) throws DataAccessException { } @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 + "]"); @@ -568,7 +561,7 @@ public String getSql() { } @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"); @@ -577,8 +570,7 @@ public int[] batchUpdate(final String... sql) throws DataAccessException { // Callback to execute the batch update. class BatchUpdateStatementCallback implements StatementCallback, SqlProvider { - @Nullable - private String currSql; + private @Nullable String currSql; @Override public int[] doInStatement(Statement stmt) throws SQLException, DataAccessException { @@ -623,8 +615,7 @@ private String appendSql(@Nullable String sql, String statement) { } @Override - @Nullable - public String getSql() { + public @Nullable String getSql() { return this.currSql; } } @@ -639,8 +630,7 @@ public String getSql() { // Methods dealing with prepared statements //------------------------------------------------------------------------- - @Nullable - private T execute(PreparedStatementCreator psc, PreparedStatementCallback action, boolean closeResources) + private @Nullable T execute(PreparedStatementCreator psc, PreparedStatementCallback action, boolean closeResources) throws DataAccessException { Assert.notNull(psc, "PreparedStatementCreator must not be null"); @@ -688,16 +678,14 @@ private T execute(PreparedStatementCreator psc, PreparedStatementCallback } @Override - @Nullable - public T execute(PreparedStatementCreator psc, PreparedStatementCallback action) + public @Nullable T execute(PreparedStatementCreator psc, PreparedStatementCallback action) throws DataAccessException { return execute(psc, action, true); } @Override - @Nullable - public T execute(String sql, PreparedStatementCallback action) throws DataAccessException { + public @Nullable T execute(String sql, PreparedStatementCallback action) throws DataAccessException { return execute(new SimplePreparedStatementCreator(sql), action, true); } @@ -712,64 +700,54 @@ public T execute(String sql, PreparedStatementCallback action) throws Dat * @return an arbitrary result object, as returned by the ResultSetExtractor * @throws DataAccessException if there is any problem */ - @Nullable - public T query( - PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss, final ResultSetExtractor rse) + public @Nullable T query( + 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 - @Nullable - public 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); } @Override - @Nullable - public T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException { + public @Nullable T query(PreparedStatementCreator psc, ResultSetExtractor rse) throws DataAccessException { return query(psc, null, rse); } @Override - @Nullable - public T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException { + public @Nullable T query(String sql, @Nullable PreparedStatementSetter pss, ResultSetExtractor rse) throws DataAccessException { return query(new SimplePreparedStatementCreator(sql), pss, rse); } @Override - @Nullable - public T query(String sql, Object[] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException { + public @Nullable T query(String sql, @Nullable Object @Nullable [] args, int[] argTypes, ResultSetExtractor rse) throws DataAccessException { return query(sql, newArgTypePreparedStatementSetter(args, argTypes), rse); } - @Deprecated + @Deprecated(since = "5.3") @Override - @Nullable - public T query(String sql, @Nullable Object[] args, ResultSetExtractor rse) throws DataAccessException { + public @Nullable T query(String sql, @Nullable Object @Nullable [] args, ResultSetExtractor rse) throws DataAccessException { return query(sql, newArgPreparedStatementSetter(args), rse); } @Override - @Nullable - public T query(String sql, ResultSetExtractor rse, @Nullable Object... args) throws DataAccessException { + public @Nullable T query(String sql, ResultSetExtractor rse, @Nullable Object @Nullable ... args) throws DataAccessException { return query(sql, newArgPreparedStatementSetter(args), rse); } @@ -784,18 +762,18 @@ public void query(String sql, @Nullable PreparedStatementSetter pss, RowCallback } @Override - public void query(String sql, Object[] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException { + public void query(String sql, @Nullable Object @Nullable [] args, int[] argTypes, RowCallbackHandler rch) throws DataAccessException { query(sql, newArgTypePreparedStatementSetter(args, argTypes), rch); } - @Deprecated + @Deprecated(since = "5.3") @Override - public void query(String sql, @Nullable Object[] args, RowCallbackHandler rch) throws DataAccessException { + public void query(String sql, @Nullable Object @Nullable [] args, RowCallbackHandler rch) throws DataAccessException { query(sql, newArgPreparedStatementSetter(args), rch); } @Override - public void query(String sql, RowCallbackHandler rch, @Nullable Object... args) throws DataAccessException { + public void query(String sql, RowCallbackHandler rch, @Nullable Object @Nullable ... args) throws DataAccessException { query(sql, newArgPreparedStatementSetter(args), rch); } @@ -810,19 +788,19 @@ public List query(String sql, @Nullable PreparedStatementSetter pss, RowM } @Override - public List query(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException { + public List query(String sql, @Nullable Object @Nullable [] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException { return result(query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper))); } - @Deprecated + @Deprecated(since = "5.3") @Override - public List query(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException { - return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + public List query(String sql, @Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException { + return result(query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper))); } @Override - public List query(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException { - return result(query(sql, args, new RowMapperResultSetExtractor<>(rowMapper))); + public List query(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { + return result(query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper))); } /** @@ -869,102 +847,96 @@ public Stream queryForStream(String sql, @Nullable PreparedStatementSette } @Override - public Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException { + public Stream queryForStream(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { return queryForStream(new SimplePreparedStatementCreator(sql), newArgPreparedStatementSetter(args), rowMapper); } @Override - @Nullable - public T queryForObject(String sql, Object[] args, int[] argTypes, RowMapper rowMapper) + public @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, int[] argTypes, RowMapper rowMapper) throws DataAccessException { List results = query(sql, args, argTypes, new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } - @Deprecated + @Deprecated(since = "5.3") @Override - @Nullable - public T queryForObject(String sql, @Nullable Object[] args, RowMapper rowMapper) throws DataAccessException { - List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + public @Nullable T queryForObject(String sql,@Nullable Object @Nullable [] args, RowMapper rowMapper) throws DataAccessException { + List results = query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } @Override - @Nullable - public T queryForObject(String sql, RowMapper rowMapper, @Nullable Object... args) throws DataAccessException { - List results = query(sql, args, new RowMapperResultSetExtractor<>(rowMapper, 1)); + public @Nullable T queryForObject(String sql, RowMapper rowMapper, @Nullable Object @Nullable ... args) throws DataAccessException { + List results = query(sql, newArgPreparedStatementSetter(args), new RowMapperResultSetExtractor<>(rowMapper, 1)); return DataAccessUtils.nullableSingleResult(results); } @Override - @Nullable - public T queryForObject(String sql, Object[] args, int[] argTypes, Class requiredType) + public @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, int[] argTypes, Class requiredType) throws DataAccessException { return queryForObject(sql, args, argTypes, getSingleColumnRowMapper(requiredType)); } - @Deprecated + @Deprecated(since = "5.3") @Override - @Nullable - public T queryForObject(String sql, @Nullable Object[] args, Class requiredType) throws DataAccessException { - return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + public @Nullable T queryForObject(String sql, @Nullable Object @Nullable [] args, Class requiredType) throws DataAccessException { + return queryForObject(sql, getSingleColumnRowMapper(requiredType), args); } @Override - @Nullable - public T queryForObject(String sql, Class requiredType, @Nullable Object... args) throws DataAccessException { - return queryForObject(sql, args, getSingleColumnRowMapper(requiredType)); + public @Nullable T queryForObject(String sql, Class requiredType, @Nullable Object @Nullable ... args) throws DataAccessException { + return queryForObject(sql, getSingleColumnRowMapper(requiredType), args); } @Override - public Map queryForMap(String sql, Object[] args, int[] argTypes) throws DataAccessException { + public Map queryForMap(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException { return result(queryForObject(sql, args, argTypes, getColumnMapRowMapper())); } @Override - public Map queryForMap(String sql, @Nullable Object... args) throws DataAccessException { - return result(queryForObject(sql, args, getColumnMapRowMapper())); + public Map queryForMap(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { + return result(queryForObject(sql, getColumnMapRowMapper(), args)); } @Override - public List queryForList(String sql, Object[] args, int[] argTypes, Class elementType) throws DataAccessException { + public List queryForList(String sql, @Nullable Object @Nullable [] args, int[] argTypes, Class elementType) throws DataAccessException { return query(sql, args, argTypes, getSingleColumnRowMapper(elementType)); } - @Deprecated + @Deprecated(since = "5.3") @Override - public List queryForList(String sql, @Nullable Object[] args, Class elementType) throws DataAccessException { - return query(sql, args, getSingleColumnRowMapper(elementType)); + public List queryForList(String sql, @Nullable Object @Nullable [] args, Class elementType) throws DataAccessException { + return query(sql, newArgPreparedStatementSetter(args), getSingleColumnRowMapper(elementType)); } @Override - public List queryForList(String sql, Class elementType, @Nullable Object... args) throws DataAccessException { - return query(sql, args, getSingleColumnRowMapper(elementType)); + public List queryForList(String sql, Class elementType, @Nullable Object @Nullable ... args) throws DataAccessException { + return query(sql, newArgPreparedStatementSetter(args), getSingleColumnRowMapper(elementType)); } @Override - public List> queryForList(String sql, Object[] args, int[] argTypes) throws DataAccessException { + public List> queryForList(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException { return query(sql, args, argTypes, getColumnMapRowMapper()); } @Override - public List> queryForList(String sql, @Nullable Object... args) throws DataAccessException { - return query(sql, args, getColumnMapRowMapper()); + public List> queryForList(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { + return query(sql, newArgPreparedStatementSetter(args), getColumnMapRowMapper()); } @Override - public SqlRowSet queryForRowSet(String sql, Object[] args, int[] argTypes) throws DataAccessException { + public SqlRowSet queryForRowSet(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException { return result(query(sql, args, argTypes, new SqlRowSetResultSetExtractor())); } @Override - public SqlRowSet queryForRowSet(String sql, @Nullable Object... args) throws DataAccessException { - return result(query(sql, args, new SqlRowSetResultSetExtractor())); + public SqlRowSet queryForRowSet(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { + return result(query(sql, newArgPreparedStatementSetter(args), new SqlRowSetResultSetExtractor())); } - protected int update(final PreparedStatementCreator psc, @Nullable final PreparedStatementSetter pss) + protected int update(PreparedStatementCreator psc, @Nullable PreparedStatementSetter pss) throws DataAccessException { logger.debug("Executing prepared SQL update"); @@ -994,7 +966,7 @@ public int update(PreparedStatementCreator psc) throws DataAccessException { } @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"); @@ -1017,18 +989,18 @@ public int update(String sql, @Nullable PreparedStatementSetter pss) throws Data } @Override - public int update(String sql, Object[] args, int[] argTypes) throws DataAccessException { + public int update(String sql, @Nullable Object @Nullable [] args, int[] argTypes) throws DataAccessException { return update(sql, newArgTypePreparedStatementSetter(args, argTypes)); } @Override - public int update(String sql, @Nullable Object... args) throws DataAccessException { + public int update(String sql, @Nullable Object @Nullable ... args) throws DataAccessException { return update(sql, newArgPreparedStatementSetter(args)); } @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)); @@ -1037,7 +1009,7 @@ public int[] batchUpdate(final PreparedStatementCreator psc, final BatchPrepared } @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 + "]"); } @@ -1057,7 +1029,7 @@ public int[] batchUpdate(String sql, List batchArgs) throws DataAccess } @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]; } @@ -1094,8 +1066,8 @@ public int getBatchSize() { } @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); @@ -1153,8 +1125,7 @@ public int[][] batchUpdate(String sql, final Collection batchArgs, final //------------------------------------------------------------------------- @Override - @Nullable - public T execute(CallableStatementCreator csc, CallableStatementCallback action) + public @Nullable T execute(CallableStatementCreator csc, CallableStatementCallback action) throws DataAccessException { Assert.notNull(csc, "CallableStatementCreator must not be null"); @@ -1200,8 +1171,7 @@ public T execute(CallableStatementCreator csc, CallableStatementCallback } @Override - @Nullable - public T execute(String callString, CallableStatementCallback action) throws DataAccessException { + public @Nullable T execute(String callString, CallableStatementCallback action) throws DataAccessException { return execute(new SimpleCallableStatementCreator(callString), action); } @@ -1261,7 +1231,7 @@ protected Map extractReturnedResults(CallableStatement cs, int rsIndex = 0; int updateIndex = 0; boolean moreResults; - if (!this.skipResultsProcessing) { + if (!isSkipResultsProcessing()) { do { if (updateCount == -1) { if (resultSetParameters != null && resultSetParameters.size() > rsIndex) { @@ -1270,7 +1240,7 @@ protected Map extractReturnedResults(CallableStatement cs, rsIndex++; } else { - if (!this.skipUndeclaredResults) { + if (!isSkipUndeclaredResults()) { String rsName = RETURN_RESULT_SET_PREFIX + (rsIndex + 1); SqlReturnResultSet undeclaredRsParam = new SqlReturnResultSet(rsName, getColumnMapRowMapper()); if (logger.isTraceEnabled()) { @@ -1289,7 +1259,7 @@ protected Map extractReturnedResults(CallableStatement cs, updateIndex++; } else { - if (!this.skipUndeclaredResults) { + if (!isSkipUndeclaredResults()) { String undeclaredName = RETURN_UPDATE_COUNT_PREFIX + (updateIndex + 1); if (logger.isTraceEnabled()) { logger.trace("Added default SqlReturnUpdateCount parameter named '" + undeclaredName + "'"); @@ -1461,7 +1431,7 @@ protected void applyStatementSettings(Statement stmt) throws SQLException { * @param args object array with arguments * @return the new PreparedStatementSetter to use */ - protected PreparedStatementSetter newArgPreparedStatementSetter(@Nullable Object[] args) { + protected PreparedStatementSetter newArgPreparedStatementSetter(@Nullable Object @Nullable [] args) { return new ArgumentPreparedStatementSetter(args); } @@ -1473,7 +1443,7 @@ protected PreparedStatementSetter newArgPreparedStatementSetter(@Nullable Object * @param argTypes int array of SQLTypes for the associated arguments * @return the new PreparedStatementSetter to use */ - protected PreparedStatementSetter newArgTypePreparedStatementSetter(Object[] args, int[] argTypes) { + protected PreparedStatementSetter newArgTypePreparedStatementSetter(@Nullable Object @Nullable [] args, int[] argTypes) { return new ArgumentTypePreparedStatementSetter(args, argTypes); } @@ -1564,8 +1534,7 @@ protected DataAccessException translateException(String task, @Nullable String s * @return the SQL string, or {@code null} if not known * @see SqlProvider */ - @Nullable - private static String getSql(Object obj) { + private static @Nullable String getSql(Object obj) { return (obj instanceof SqlProvider sqlProvider ? sqlProvider.getSql() : null); } @@ -1662,8 +1631,7 @@ public CloseSuppressingInvocationHandler(Connection target) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ConnectionProxy interface coming in... return switch (method.getName()) { @@ -1764,8 +1732,7 @@ public RowCallbackHandlerResultSetExtractor(RowCallbackHandler rch) { } @Override - @Nullable - public Object extractData(ResultSet rs) throws SQLException { + public @Nullable Object extractData(ResultSet rs) throws SQLException { while (rs.next()) { this.rch.processRow(rs); } @@ -1806,8 +1773,7 @@ public boolean tryAdvance(Consumer action) { } @Override - @Nullable - public Spliterator trySplit() { + public @Nullable Spliterator trySplit() { return null; } 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 97c9f32aa98a..23d26f61aea9 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. @@ -19,8 +19,9 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Generic callback interface for code that operates on a PreparedStatement. @@ -71,10 +72,9 @@ 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; + @Nullable T doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java index ddc9ac073c63..6d174ad8048c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/PreparedStatementCreatorFactory.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. @@ -29,8 +29,9 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; /** * Helper class that efficiently creates multiple {@link PreparedStatementCreator} @@ -47,8 +48,7 @@ public class PreparedStatementCreatorFactory { private final String sql; /** List of SqlParameter objects (may be {@code null}). */ - @Nullable - private List declaredParameters; + private @Nullable List declaredParameters; private int resultSetType = ResultSet.TYPE_FORWARD_ONLY; @@ -56,8 +56,7 @@ public class PreparedStatementCreatorFactory { private boolean returnGeneratedKeys = false; - @Nullable - private String[] generatedKeysColumnNames; + private String @Nullable [] generatedKeysColumnNames; /** @@ -155,7 +154,7 @@ public PreparedStatementSetter newPreparedStatementSetter(@Nullable List para * Return a new PreparedStatementSetter for the given parameters. * @param params the parameter array (may be {@code null}) */ - public PreparedStatementSetter newPreparedStatementSetter(@Nullable Object[] params) { + public PreparedStatementSetter newPreparedStatementSetter(@Nullable Object @Nullable [] params) { return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList()); } @@ -163,7 +162,7 @@ public PreparedStatementSetter newPreparedStatementSetter(@Nullable Object[] par * Return a new PreparedStatementCreator for the given parameters. * @param params list of parameters (may be {@code null}) */ - public PreparedStatementCreator newPreparedStatementCreator(@Nullable List params) { + public PreparedStatementCreator newPreparedStatementCreator(@Nullable List params) { return new PreparedStatementCreatorImpl(params != null ? params : Collections.emptyList()); } @@ -171,7 +170,7 @@ public PreparedStatementCreator newPreparedStatementCreator(@Nullable List pa * Return a new PreparedStatementCreator for the given parameters. * @param params the parameter array (may be {@code null}) */ - public PreparedStatementCreator newPreparedStatementCreator(@Nullable Object[] params) { + public PreparedStatementCreator newPreparedStatementCreator(@Nullable Object @Nullable [] params) { return new PreparedStatementCreatorImpl(params != null ? Arrays.asList(params) : Collections.emptyList()); } @@ -181,7 +180,7 @@ public PreparedStatementCreator newPreparedStatementCreator(@Nullable Object[] p * the factory's, for example because of named parameter expanding) * @param params the parameter array (may be {@code null}) */ - public PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, @Nullable Object[] params) { + public PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, @Nullable Object @Nullable [] params) { return new PreparedStatementCreatorImpl( sqlToUse, (params != null ? Arrays.asList(params) : Collections.emptyList())); } @@ -209,7 +208,7 @@ public PreparedStatementCreatorImpl(String actualSql, List parameters) { Set names = new HashSet<>(); for (int i = 0; i < parameters.size(); i++) { Object param = parameters.get(i); - if (param instanceof SqlParameterValue sqlParameterValue) { + if (param instanceof SqlParameterValue sqlParameterValue && sqlParameterValue.getName() != null) { names.add(sqlParameterValue.getName()); } else { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java index 0acaf0620df8..33a7ba541df2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetExtractor.java @@ -19,8 +19,9 @@ import java.sql.ResultSet; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Callback interface used by {@link JdbcTemplate}'s query methods. @@ -61,7 +62,6 @@ public interface ResultSetExtractor { * values or navigating (that is, there's no need to catch SQLException) * @throws DataAccessException in case of custom exceptions */ - @Nullable - T extractData(ResultSet rs) throws SQLException, DataAccessException; + @Nullable T extractData(ResultSet rs) throws SQLException, DataAccessException; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java index 0c8b9b42b610..3ef1703bf77e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/ResultSetSupportingSqlParameter.java @@ -18,7 +18,7 @@ import java.sql.ResultSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Common base class for ResultSet-supporting SqlParameters like @@ -29,14 +29,11 @@ */ public class ResultSetSupportingSqlParameter extends SqlParameter { - @Nullable - private ResultSetExtractor resultSetExtractor; + private @Nullable ResultSetExtractor resultSetExtractor; - @Nullable - private RowCallbackHandler rowCallbackHandler; + private @Nullable RowCallbackHandler rowCallbackHandler; - @Nullable - private RowMapper rowMapper; + private @Nullable RowMapper rowMapper; /** @@ -114,24 +111,21 @@ public boolean isResultSetSupported() { /** * Return the ResultSetExtractor held by this parameter, if any. */ - @Nullable - public ResultSetExtractor getResultSetExtractor() { + public @Nullable ResultSetExtractor getResultSetExtractor() { return this.resultSetExtractor; } /** * Return the RowCallbackHandler held by this parameter, if any. */ - @Nullable - public RowCallbackHandler getRowCallbackHandler() { + public @Nullable RowCallbackHandler getRowCallbackHandler() { return this.rowCallbackHandler; } /** * Return the RowMapper held by this parameter, if any. */ - @Nullable - public RowMapper getRowMapper() { + public @Nullable RowMapper getRowMapper() { return this.rowMapper; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java index 2d2b09ac9a98..35161ec92bb9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowCountCallbackHandler.java @@ -20,8 +20,9 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; /** * Implementation of RowCallbackHandler. Convenient superclass for callback handlers. @@ -55,14 +56,12 @@ public class RowCountCallbackHandler implements RowCallbackHandler { * Indexed from 0. Type (as in java.sql.Types) for the columns * as returned by ResultSetMetaData object. */ - @Nullable - private int[] columnTypes; + private int @Nullable [] columnTypes; /** * Indexed from 0. Column name as returned by ResultSetMetaData object. */ - @Nullable - private String[] columnNames; + private String @Nullable [] columnNames; /** @@ -105,8 +104,7 @@ protected void processRow(ResultSet rs, int rowNum) throws SQLException { * @return the types of the columns as java.sql.Types constants. * Indexed from 0 to n-1. */ - @Nullable - public final int[] getColumnTypes() { + public final int @Nullable [] getColumnTypes() { return this.columnTypes; } @@ -116,8 +114,7 @@ public final int[] getColumnTypes() { * @return the names of the columns. * Indexed from 0 to n-1. */ - @Nullable - public final String[] getColumnNames() { + public final String @Nullable [] getColumnNames() { return this.columnNames; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java index 291d3f2ac1a4..92cbb414c8e1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/RowMapper.java @@ -19,7 +19,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An interface used by {@link JdbcTemplate} for mapping rows of a @@ -61,7 +61,6 @@ public interface RowMapper { * @throws SQLException if an SQLException is encountered while getting * column values (that is, there's no need to catch SQLException) */ - @Nullable - T mapRow(ResultSet rs, int rowNum) throws SQLException; + @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java index e15c8e21e3a2..c5e59763dedb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SimplePropertyRowMapper.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.Set; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; @@ -82,7 +84,7 @@ public class SimplePropertyRowMapper implements RowMapper { private final Constructor mappedConstructor; - private final String[] constructorParameterNames; + private final @Nullable String[] constructorParameterNames; private final TypeDescriptor[] constructorParameterTypes; @@ -122,7 +124,7 @@ public SimplePropertyRowMapper(Class mappedClass, ConversionService conversio @Override public T mapRow(ResultSet rs, int rowNumber) throws SQLException { - Object[] args = new Object[this.constructorParameterNames.length]; + @Nullable Object[] args = new Object[this.constructorParameterNames.length]; Set usedIndex = new HashSet<>(); for (int i = 0; i < args.length; i++) { String name = this.constructorParameterNames[i]; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java index 43dffc066eba..5ecea46a8d23 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SingleColumnRowMapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,12 +20,13 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.dao.TypeMismatchDataAccessException; import org.springframework.jdbc.IncorrectResultSetColumnCountException; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.NumberUtils; @@ -47,11 +48,9 @@ */ public class SingleColumnRowMapper implements RowMapper { - @Nullable - private Class requiredType; + private @Nullable Class requiredType; - @Nullable - private ConversionService conversionService = DefaultConversionService.getSharedInstance(); + private @Nullable ConversionService conversionService = DefaultConversionService.getSharedInstance(); /** @@ -71,6 +70,19 @@ public SingleColumnRowMapper(Class requiredType) { } } + /** + * Create a new {@code SingleColumnRowMapper}. + * @param requiredType the type that each result object is expected to match + * @param conversionService a {@link ConversionService} for converting a fetched value + * @since 7.0 + */ + public SingleColumnRowMapper(Class requiredType, @Nullable ConversionService conversionService) { + if (requiredType != Object.class) { + setRequiredType(requiredType); + } + setConversionService(conversionService); + } + /** * Set the type that each result object is expected to match. @@ -85,12 +97,13 @@ public void setRequiredType(Class requiredType) { * Set a {@link ConversionService} for converting a fetched value. *

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

    Validates that there is only one column selected, @@ -102,8 +115,7 @@ public void setConversionService(@Nullable ConversionService conversionService) */ @Override @SuppressWarnings("unchecked") - @Nullable - public T mapRow(ResultSet rs, int rowNum) throws SQLException { + public @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException { // Validate column count. ResultSetMetaData rsmd = rs.getMetaData(); int nrOfColumns = rsmd.getColumnCount(); @@ -144,8 +156,7 @@ public T mapRow(ResultSet rs, int rowNum) throws SQLException { * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int, Class) * @see #getColumnValue(java.sql.ResultSet, int) */ - @Nullable - protected Object getColumnValue(ResultSet rs, int index, @Nullable Class requiredType) throws SQLException { + protected @Nullable Object getColumnValue(ResultSet rs, int index, @Nullable Class requiredType) throws SQLException { if (requiredType != null) { return JdbcUtils.getResultSetValue(rs, index, requiredType); } @@ -169,8 +180,7 @@ protected Object getColumnValue(ResultSet rs, int index, @Nullable Class requ * @throws SQLException in case of extraction failure * @see org.springframework.jdbc.support.JdbcUtils#getResultSetValue(java.sql.ResultSet, int) */ - @Nullable - protected Object getColumnValue(ResultSet rs, int index) throws SQLException { + protected @Nullable Object getColumnValue(ResultSet rs, int index) throws SQLException { return JdbcUtils.getResultSetValue(rs, index); } @@ -190,8 +200,7 @@ protected Object getColumnValue(ResultSet rs, int index) throws SQLException { * @see #getColumnValue(java.sql.ResultSet, int, Class) */ @SuppressWarnings("unchecked") - @Nullable - protected Object convertValueToRequiredType(Object value, Class requiredType) { + protected @Nullable Object convertValueToRequiredType(Object value, Class requiredType) { if (String.class == requiredType) { return value.toString(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java index a43ec1a0dd86..c7b177a42b92 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlOutParameter.java @@ -18,7 +18,7 @@ import java.sql.ResultSet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Subclass of {@link SqlParameter} to represent an output parameter. @@ -34,8 +34,7 @@ */ public class SqlOutParameter extends ResultSetSupportingSqlParameter { - @Nullable - private SqlReturnType sqlReturnType; + private @Nullable SqlReturnType sqlReturnType; /** @@ -114,8 +113,7 @@ public SqlOutParameter(String name, int sqlType, RowMapper rm) { /** * Return the custom return type, if any. */ - @Nullable - public SqlReturnType getSqlReturnType() { + public @Nullable SqlReturnType getSqlReturnType() { return this.sqlReturnType; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java index 339611df6849..609ed04b31f2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameter.java @@ -19,7 +19,8 @@ import java.util.ArrayList; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -36,19 +37,16 @@ public class SqlParameter { // The name of the parameter, if any - @Nullable - private String name; + private @Nullable String name; // SQL type constant from {@code java.sql.Types} private final int sqlType; // Used for types that are user-named like: STRUCT, DISTINCT, JAVA_OBJECT, named array types - @Nullable - private String typeName; + private @Nullable String typeName; // The scale to apply in case of a NUMERIC or DECIMAL type, if any - @Nullable - private Integer scale; + private @Nullable Integer scale; /** @@ -131,8 +129,7 @@ public SqlParameter(SqlParameter otherParam) { /** * Return the name of the parameter, or {@code null} if anonymous. */ - @Nullable - public String getName() { + public @Nullable String getName() { return this.name; } @@ -146,16 +143,14 @@ public int getSqlType() { /** * Return the type name of the parameter, if any. */ - @Nullable - public String getTypeName() { + public @Nullable String getTypeName() { return this.typeName; } /** * Return the scale of the parameter, if any. */ - @Nullable - public Integer getScale() { + public @Nullable Integer getScale() { return this.scale; } @@ -183,7 +178,7 @@ public boolean isResultsParameter() { * Convert a list of JDBC types, as defined in {@code java.sql.Types}, * to a List of SqlParameter objects as used in this package. */ - public static List sqlTypesToAnonymousParameterList(@Nullable int... types) { + public static List sqlTypesToAnonymousParameterList(int @Nullable ... types) { if (types == null) { return new ArrayList<>(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java index 30159fe4dcee..d693b8bc8bc0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlParameterValue.java @@ -16,7 +16,7 @@ package org.springframework.jdbc.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Object to represent an SQL parameter value, including parameter meta-data @@ -38,8 +38,7 @@ */ public class SqlParameterValue extends SqlParameter { - @Nullable - private final Object value; + private final @Nullable Object value; /** @@ -89,8 +88,7 @@ public SqlParameterValue(SqlParameter declaredParam, @Nullable Object value) { /** * Return the value object that this parameter value holds. */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java index e060bc1522a3..01b8c0b2267e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlProvider.java @@ -16,7 +16,7 @@ package org.springframework.jdbc.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to be implemented by objects that can provide SQL strings. @@ -38,7 +38,6 @@ public interface SqlProvider { * typically the SQL used for creating statements. * @return the SQL string, or {@code null} if not available */ - @Nullable - String getSql(); + @Nullable String getSql(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java index f2558c669116..c2d609b7b584 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlReturnType.java @@ -19,7 +19,7 @@ import java.sql.CallableStatement; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to be implemented for retrieving values for more complex database-specific diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java index 2349ac1cb2f6..8c30cda0937c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/SqlTypeValue.java @@ -19,8 +19,9 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; /** * Interface to be implemented for setting values for more complex database-specific diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java index 1297b94dea2d..907027d5d3f0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/StatementCallback.java @@ -19,8 +19,9 @@ import java.sql.SQLException; import java.sql.Statement; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Generic callback interface for code that operates on a JDBC Statement. @@ -67,7 +68,6 @@ public interface StatementCallback { * @see JdbcTemplate#queryForObject(String, Class) * @see JdbcTemplate#queryForRowSet(String) */ - @Nullable - T doInStatement(Statement stmt) throws SQLException, DataAccessException; + @Nullable T doInStatement(Statement stmt) 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 3eea1df60613..bd559b6e9338 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. @@ -40,10 +40,10 @@ 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.jdbc.support.SqlValue; -import org.springframework.lang.Nullable; /** * Utility methods for PreparedStatementSetter/Creator and CallableStatementCreator @@ -85,8 +85,7 @@ public abstract class StatementCreatorUtils { private static final Map, Integer> javaTypeToSqlTypeMap = new HashMap<>(64); - @Nullable - static Boolean shouldIgnoreGetParameterType; + static @Nullable Boolean shouldIgnoreGetParameterType = SpringProperties.checkFlag(IGNORE_GETPARAMETERTYPE_PROPERTY_NAME); static { javaTypeToSqlTypeMap.put(boolean.class, Types.BOOLEAN); @@ -115,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); - } } @@ -494,7 +488,7 @@ private static boolean isDateValue(Class inValueType) { * @see DisposableSqlTypeValue#cleanup() * @see org.springframework.jdbc.core.support.SqlLobValue#cleanup() */ - public static void cleanupParameters(@Nullable Object... paramValues) { + public static void cleanupParameters(@Nullable Object @Nullable ... paramValues) { if (paramValues != null) { cleanupParameters(Arrays.asList(paramValues)); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java index fdc66aa1c342..d291aa37df50 100755 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataContext.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.RowMapper; @@ -38,7 +39,6 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -58,23 +58,19 @@ public class CallMetaDataContext { protected final Log logger = LogFactory.getLog(getClass()); // Name of procedure to call - @Nullable - private String procedureName; + private @Nullable String procedureName; // Name of catalog for call - @Nullable - private String catalogName; + private @Nullable String catalogName; // Name of schema for call - @Nullable - private String schemaName; + private @Nullable String schemaName; // List of SqlParameter objects to be used in call execution private List callParameters = new ArrayList<>(); // Actual name to use for the return value in the output map - @Nullable - private String actualFunctionReturnName; + private @Nullable String actualFunctionReturnName; // Set of in parameter names to exclude use for any not listed private Set limitedInParameterNames = new HashSet<>(); @@ -95,8 +91,7 @@ public class CallMetaDataContext { private boolean namedBinding; // The provider of call meta-data - @Nullable - private CallMetaDataProvider metaDataProvider; + private @Nullable CallMetaDataProvider metaDataProvider; /** @@ -151,8 +146,7 @@ public void setProcedureName(@Nullable String procedureName) { /** * Get the name of the procedure. */ - @Nullable - public String getProcedureName() { + public @Nullable String getProcedureName() { return this.procedureName; } @@ -166,8 +160,7 @@ public void setCatalogName(@Nullable String catalogName) { /** * Get the name of the catalog. */ - @Nullable - public String getCatalogName() { + public @Nullable String getCatalogName() { return this.catalogName; } @@ -181,8 +174,7 @@ public void setSchemaName(@Nullable String schemaName) { /** * Get the name of the schema. */ - @Nullable - public String getSchemaName() { + public @Nullable String getSchemaName() { return this.schemaName; } @@ -285,8 +277,7 @@ public SqlParameter createReturnResultSetParameter(String parameterName, RowMapp * Get the name of the single out parameter for this call. * If there are multiple parameters, the name of the first one will be returned. */ - @Nullable - public String getScalarOutParameterName() { + public @Nullable String getScalarOutParameterName() { if (isFunction()) { return getFunctionReturnName(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java index a13a43161ffc..1d564f16bfc2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallMetaDataProvider.java @@ -20,8 +20,9 @@ import java.sql.SQLException; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; /** * Interface specifying the API to be implemented by a class providing call meta-data. @@ -67,46 +68,40 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N * Provide any modification of the procedure name passed in to match the meta-data currently used. *

    This could include altering the case. */ - @Nullable - String procedureNameToUse(@Nullable String procedureName); + @Nullable String procedureNameToUse(@Nullable String procedureName); /** * Provide any modification of the catalog name passed in to match the meta-data currently used. *

    This could include altering the case. */ - @Nullable - String catalogNameToUse(@Nullable String catalogName); + @Nullable String catalogNameToUse(@Nullable String catalogName); /** * Provide any modification of the schema name passed in to match the meta-data currently used. *

    This could include altering the case. */ - @Nullable - String schemaNameToUse(@Nullable String schemaName); + @Nullable String schemaNameToUse(@Nullable String schemaName); /** * Provide any modification of the catalog name passed in to match the meta-data currently used. *

    The returned value will be used for meta-data lookups. This could include altering the case * used or providing a base catalog if none is provided. */ - @Nullable - String metaDataCatalogNameToUse(@Nullable String catalogName) ; + @Nullable String metaDataCatalogNameToUse(@Nullable String catalogName) ; /** * Provide any modification of the schema name passed in to match the meta-data currently used. *

    The returned value will be used for meta-data lookups. This could include altering the case * used or providing a base schema if none is provided. */ - @Nullable - String metaDataSchemaNameToUse(@Nullable String schemaName); + @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName); /** * Provide any modification of the column name passed in to match the meta-data currently used. *

    This could include altering the case. * @param parameterName name of the parameter of column */ - @Nullable - String parameterNameToUse(@Nullable String parameterName); + @Nullable String parameterNameToUse(@Nullable String parameterName); /** * Return the name of the named parameter to use for binding the given parameter name. @@ -147,8 +142,7 @@ void initializeWithProcedureColumnMetaData(DatabaseMetaData databaseMetaData, @N * Get the name of the current user. Useful for meta-data lookups etc. * @return current user name from database connection */ - @Nullable - String getUserName(); + @Nullable String getUserName(); /** * Are we using the meta-data for the procedure columns? diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java index 2b7a61c069d7..5103f034911b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/CallParameterMetaData.java @@ -18,7 +18,7 @@ import java.sql.DatabaseMetaData; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Holder of meta-data for a specific parameter that is used for call processing. @@ -32,15 +32,13 @@ public class CallParameterMetaData { private final boolean function; - @Nullable - private final String parameterName; + private final @Nullable String parameterName; private final int parameterType; private final int sqlType; - @Nullable - private final String typeName; + private final @Nullable String typeName; private final boolean nullable; @@ -72,8 +70,7 @@ public boolean isFunction() { /** * Return the parameter name. */ - @Nullable - public String getParameterName() { + public @Nullable String getParameterName() { return this.parameterName; } @@ -129,8 +126,7 @@ public int getSqlType() { /** * Return the parameter type name. */ - @Nullable - public String getTypeName() { + public @Nullable String getTypeName() { return this.typeName; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java index 66c49564681c..0de476af1f0d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/Db2CallMetaDataProvider.java @@ -20,7 +20,7 @@ import java.sql.SQLException; import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * DB2 specific implementation for the {@link CallMetaDataProvider} interface. @@ -66,8 +66,7 @@ public void initializeWithMetaData(DatabaseMetaData databaseMetaData) throws SQL } @Override - @Nullable - public String metaDataSchemaNameToUse(@Nullable String schemaName) { + public @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) { if (schemaName != null) { return super.metaDataSchemaNameToUse(schemaName); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java index efaa556cf6b3..84767aa76f2d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/DerbyCallMetaDataProvider.java @@ -20,7 +20,7 @@ import java.sql.SQLException; import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Derby specific implementation for the {@link CallMetaDataProvider} interface. @@ -38,8 +38,7 @@ public DerbyCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLEx @Override - @Nullable - public String metaDataSchemaNameToUse(@Nullable String schemaName) { + public @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) { if (schemaName != null) { return super.metaDataSchemaNameToUse(schemaName); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java index 5eb124b063f6..96de2873a261 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProvider.java @@ -26,12 +26,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.SqlInOutParameter; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -125,26 +125,22 @@ public List getCallParameterMetaData() { } @Override - @Nullable - public String procedureNameToUse(@Nullable String procedureName) { + public @Nullable String procedureNameToUse(@Nullable String procedureName) { return identifierNameToUse(procedureName); } @Override - @Nullable - public String catalogNameToUse(@Nullable String catalogName) { + public @Nullable String catalogNameToUse(@Nullable String catalogName) { return identifierNameToUse(catalogName); } @Override - @Nullable - public String schemaNameToUse(@Nullable String schemaName) { + public @Nullable String schemaNameToUse(@Nullable String schemaName) { return identifierNameToUse(schemaName); } @Override - @Nullable - public String metaDataCatalogNameToUse(@Nullable String catalogName) { + public @Nullable String metaDataCatalogNameToUse(@Nullable String catalogName) { if (isSupportsCatalogsInProcedureCalls()) { return catalogNameToUse(catalogName); } @@ -154,8 +150,7 @@ public String metaDataCatalogNameToUse(@Nullable String catalogName) { } @Override - @Nullable - public String metaDataSchemaNameToUse(@Nullable String schemaName) { + public @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) { if (isSupportsSchemasInProcedureCalls()) { return schemaNameToUse(schemaName); } @@ -165,8 +160,7 @@ public String metaDataSchemaNameToUse(@Nullable String schemaName) { } @Override - @Nullable - public String parameterNameToUse(@Nullable String parameterName) { + public @Nullable String parameterNameToUse(@Nullable String parameterName) { return identifierNameToUse(parameterName); } @@ -279,8 +273,7 @@ protected boolean isStoresLowerCaseIdentifiers() { } - @Nullable - private String identifierNameToUse(@Nullable String identifierName) { + private @Nullable String identifierNameToUse(@Nullable String identifierName) { if (identifierName == null) { return null; } @@ -438,8 +431,7 @@ private ProcedureMetadata getProcedureMetadataAsFunction(DatabaseMetaData databa return new ProcedureMetadata(schemaName, procedureName, matches, true); } - @Nullable - private static String escapeNamePattern(@Nullable String name, @Nullable String escape) { + private static @Nullable String escapeNamePattern(@Nullable String name, @Nullable String escape) { if (name == null || escape == null) { return name; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java index b5e65f418b9b..defbd02b89ec 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/GenericTableMetaDataProvider.java @@ -29,10 +29,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; /** * A generic implementation of the {@link TableMetaDataProvider} interface @@ -54,12 +54,10 @@ public class GenericTableMetaDataProvider implements TableMetaDataProvider { /** The name of the user currently connected. */ - @Nullable - private final String userName; + private final @Nullable String userName; /** The version of the database. */ - @Nullable - private String databaseVersion; + private @Nullable String databaseVersion; /** Indicates whether column meta-data has been used. */ private boolean tableColumnMetaDataUsed = false; @@ -186,31 +184,26 @@ public List getTableParameterMetaData() { } @Override - @Nullable - public String tableNameToUse(@Nullable String tableName) { + public @Nullable String tableNameToUse(@Nullable String tableName) { return identifierNameToUse(tableName); } @Override - @Nullable - public String columnNameToUse(@Nullable String columnName) { + public @Nullable String columnNameToUse(@Nullable String columnName) { return identifierNameToUse(columnName); } @Override - @Nullable - public String catalogNameToUse(@Nullable String catalogName) { + public @Nullable String catalogNameToUse(@Nullable String catalogName) { return identifierNameToUse(catalogName); } @Override - @Nullable - public String schemaNameToUse(@Nullable String schemaName) { + public @Nullable String schemaNameToUse(@Nullable String schemaName) { return identifierNameToUse(schemaName); } - @Nullable - private String identifierNameToUse(@Nullable String identifierName) { + private @Nullable String identifierNameToUse(@Nullable String identifierName) { if (identifierName == null) { return null; } @@ -226,14 +219,12 @@ else if (isStoresLowerCaseIdentifiers()) { } @Override - @Nullable - public String metaDataCatalogNameToUse(@Nullable String catalogName) { + public @Nullable String metaDataCatalogNameToUse(@Nullable String catalogName) { return catalogNameToUse(catalogName); } @Override - @Nullable - public String metaDataSchemaNameToUse(@Nullable String schemaName) { + public @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) { if (schemaName == null) { return schemaNameToUse(getDefaultSchema()); } @@ -243,16 +234,14 @@ public String metaDataSchemaNameToUse(@Nullable String schemaName) { /** * Provide access to the default schema for subclasses. */ - @Nullable - protected String getDefaultSchema() { + protected @Nullable String getDefaultSchema() { return this.userName; } /** * Provide access to the version info for subclasses. */ - @Nullable - protected String getDatabaseVersion() { + protected @Nullable String getDatabaseVersion() { return this.databaseVersion; } @@ -276,8 +265,7 @@ public boolean isGetGeneratedKeysSimulated(){ } @Override - @Nullable - public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + public @Nullable String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java index f655eb090347..cc64c1a3be22 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleCallMetaDataProvider.java @@ -20,10 +20,11 @@ import java.sql.SQLException; import java.sql.Types; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; /** * Oracle-specific implementation for the {@link CallMetaDataProvider} interface. @@ -58,15 +59,13 @@ public int getRefCursorSqlType() { } @Override - @Nullable - public String metaDataCatalogNameToUse(@Nullable String catalogName) { + public @Nullable String metaDataCatalogNameToUse(@Nullable String catalogName) { // Oracle uses catalog name for package name or an empty string if no package return (catalogName == null ? "" : catalogNameToUse(catalogName)); } @Override - @Nullable - public String metaDataSchemaNameToUse(@Nullable String schemaName) { + public @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) { // Use current user schema if no schema specified return (schemaName == null ? getUserName() : super.metaDataSchemaNameToUse(schemaName)); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java index 5374099fee64..b2535c1c519c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/OracleTableMetaDataProvider.java @@ -23,8 +23,9 @@ import java.sql.SQLException; import java.sql.Types; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -43,8 +44,7 @@ public class OracleTableMetaDataProvider extends GenericTableMetaDataProvider { private final boolean includeSynonyms; - @Nullable - private final String defaultSchema; + private final @Nullable String defaultSchema; /** @@ -72,8 +72,7 @@ public OracleTableMetaDataProvider(DatabaseMetaData databaseMetaData, boolean in /* * Oracle-based implementation for detecting the current schema. */ - @Nullable - private static String lookupDefaultSchema(DatabaseMetaData databaseMetaData) { + private static @Nullable String lookupDefaultSchema(DatabaseMetaData databaseMetaData) { try { CallableStatement cstmt = null; try { @@ -100,8 +99,7 @@ private static String lookupDefaultSchema(DatabaseMetaData databaseMetaData) { } @Override - @Nullable - protected String getDefaultSchema() { + protected @Nullable String getDefaultSchema() { if (this.defaultSchema != null) { return this.defaultSchema; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java index 31275e90ebbf..93a77e88149e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/PostgresCallMetaDataProvider.java @@ -20,10 +20,11 @@ import java.sql.SQLException; import java.sql.Types; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.ColumnMapRowMapper; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; /** * Postgres-specific implementation for the {@link CallMetaDataProvider} interface. @@ -65,8 +66,7 @@ public int getRefCursorSqlType() { } @Override - @Nullable - public String metaDataSchemaNameToUse(@Nullable String schemaName) { + public @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) { return (schemaName == null ? this.schemaName : super.metaDataSchemaNameToUse(schemaName)); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java index 5e745930812e..794de56f2475 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SqlServerCallMetaDataProvider.java @@ -19,7 +19,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * SQL Server specific implementation for the {@link CallMetaDataProvider} interface. @@ -42,8 +42,7 @@ public SqlServerCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws S @Override - @Nullable - public String parameterNameToUse(@Nullable String parameterName) { + public @Nullable String parameterNameToUse(@Nullable String parameterName) { if (parameterName == null) { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java index 566361531209..7e3b454b819c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/SybaseCallMetaDataProvider.java @@ -19,7 +19,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Sybase specific implementation for the {@link CallMetaDataProvider} interface. @@ -42,8 +42,7 @@ public SybaseCallMetaDataProvider(DatabaseMetaData databaseMetaData) throws SQLE @Override - @Nullable - public String parameterNameToUse(@Nullable String parameterName) { + public @Nullable String parameterNameToUse(@Nullable String parameterName) { if (parameterName == null) { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java index e7cb34de8fb9..f31e7ef68d74 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataContext.java @@ -27,13 +27,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.SqlTypeValue; import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.core.namedparam.SqlParameterSourceUtils; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -53,16 +53,13 @@ public class TableMetaDataContext { protected final Log logger = LogFactory.getLog(getClass()); // Name of table for this context - @Nullable - private String tableName; + private @Nullable String tableName; // Name of catalog for this context - @Nullable - private String catalogName; + private @Nullable String catalogName; // Name of schema for this context - @Nullable - private String schemaName; + private @Nullable String schemaName; // Should we access insert parameter meta-data info or not private boolean accessTableColumnMetaData = true; @@ -74,8 +71,7 @@ public class TableMetaDataContext { private boolean quoteIdentifiers = false; // The provider of table meta-data - @Nullable - private TableMetaDataProvider metaDataProvider; + private @Nullable TableMetaDataProvider metaDataProvider; // List of columns objects to be used in this context private List tableColumns = new ArrayList<>(); @@ -94,8 +90,7 @@ public void setTableName(@Nullable String tableName) { /** * Get the name of the table for this context. */ - @Nullable - public String getTableName() { + public @Nullable String getTableName() { return this.tableName; } @@ -109,8 +104,7 @@ public void setCatalogName(@Nullable String catalogName) { /** * Get the name of the catalog for this context. */ - @Nullable - public String getCatalogName() { + public @Nullable String getCatalogName() { return this.catalogName; } @@ -124,8 +118,7 @@ public void setSchemaName(@Nullable String schemaName) { /** * Get the name of the schema for this context. */ - @Nullable - public String getSchemaName() { + public @Nullable String getSchemaName() { return this.schemaName; } @@ -411,8 +404,7 @@ public boolean isGetGeneratedKeysSimulated() { * retrieving generated keys is not supported. * @see #isGetGeneratedKeysSimulated() */ - @Nullable - public String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { + public @Nullable String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName) { return obtainMetaDataProvider().getSimpleQueryForGetGeneratedKey(tableName, keyColumnName); } @@ -428,8 +420,7 @@ public boolean isGeneratedKeysColumnNameArraySupported() { private static final class QuoteHandler { - @Nullable - private final String identifierQuoteString; + private final @Nullable String identifierQuoteString; private final boolean quoting; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java index 1af80eec37f5..299d782ca351 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/TableMetaDataProvider.java @@ -20,7 +20,7 @@ import java.sql.SQLException; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface specifying the API to be implemented by a class providing table meta-data. @@ -63,30 +63,26 @@ void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, @Nulla * Get the table name formatted based on meta-data information. *

    This could include altering the case. */ - @Nullable - String tableNameToUse(@Nullable String tableName); + @Nullable String tableNameToUse(@Nullable String tableName); /** * Get the column name formatted based on meta-data information. *

    This could include altering the case. * @since 6.1 */ - @Nullable - String columnNameToUse(@Nullable String columnName); + @Nullable String columnNameToUse(@Nullable String columnName); /** * Get the catalog name formatted based on meta-data information. *

    This could include altering the case. */ - @Nullable - String catalogNameToUse(@Nullable String catalogName); + @Nullable String catalogNameToUse(@Nullable String catalogName); /** * Get the schema name formatted based on meta-data information. *

    This could include altering the case. */ - @Nullable - String schemaNameToUse(@Nullable String schemaName); + @Nullable String schemaNameToUse(@Nullable String schemaName); /** * Provide any modification of the catalog name passed in to match the meta-data @@ -95,8 +91,7 @@ void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, @Nulla *

    This could include altering the case used or providing a base catalog * if none is provided. */ - @Nullable - String metaDataCatalogNameToUse(@Nullable String catalogName) ; + @Nullable String metaDataCatalogNameToUse(@Nullable String catalogName) ; /** * Provide any modification of the schema name passed in to match the meta-data @@ -105,8 +100,7 @@ void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, @Nulla *

    This could include altering the case used or providing a base schema * if none is provided. */ - @Nullable - String metaDataSchemaNameToUse(@Nullable String schemaName) ; + @Nullable String metaDataSchemaNameToUse(@Nullable String schemaName) ; /** * Are we using the meta-data for the table columns? @@ -132,8 +126,7 @@ void initializeWithTableColumnMetaData(DatabaseMetaData databaseMetaData, @Nulla * retrieving generated keys is not supported. * @see #isGetGeneratedKeysSimulated() */ - @Nullable - String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName); + @Nullable String getSimpleQueryForGetGeneratedKey(String tableName, String keyColumnName); /** * Does this database support a column name String array for retrieving generated diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java index 3627e646b39e..909f18d6f721 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/metadata/package-info.java @@ -2,9 +2,7 @@ * Context metadata abstraction for the configuration and execution * of table inserts and stored procedure calls. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.core.metadata; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java index a68f7899ca9c..4766feea574f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/AbstractSqlParameterSource.java @@ -20,9 +20,10 @@ import java.util.Map; import java.util.StringJoiner; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.SqlParameterValue; import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -85,8 +86,7 @@ public int getSqlType(String paramName) { * or {@code null} if not registered */ @Override - @Nullable - public String getTypeName(String paramName) { + public @Nullable String getTypeName(String paramName) { Assert.notNull(paramName, "Parameter name must not be null"); return this.typeNames.get(paramName); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java index bb2c595b6534..f3fdc3f67d96 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/BeanPropertySqlParameterSource.java @@ -20,13 +20,13 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanWrapper; import org.springframework.beans.NotReadablePropertyException; import org.springframework.beans.PropertyAccessor; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.jdbc.core.StatementCreatorUtils; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -47,8 +47,7 @@ public class BeanPropertySqlParameterSource extends AbstractSqlParameterSource { private final BeanWrapper beanWrapper; - @Nullable - private String[] propertyNames; + private String @Nullable [] propertyNames; /** @@ -66,8 +65,7 @@ public boolean hasValue(String paramName) { } @Override - @Nullable - public Object getValue(String paramName) throws IllegalArgumentException { + public @Nullable Object getValue(String paramName) throws IllegalArgumentException { try { return this.beanWrapper.getPropertyValue(paramName); } @@ -91,7 +89,6 @@ public int getSqlType(String paramName) { } @Override - @NonNull public String[] getParameterNames() { return getReadablePropertyNames(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java index f3034c9daa9b..e4a8d09e82e4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/EmptySqlParameterSource.java @@ -16,7 +16,7 @@ package org.springframework.jdbc.core.namedparam; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A simple empty implementation of the {@link SqlParameterSource} interface. @@ -38,8 +38,7 @@ public boolean hasValue(String paramName) { } @Override - @Nullable - public Object getValue(String paramName) throws IllegalArgumentException { + public @Nullable Object getValue(String paramName) throws IllegalArgumentException { throw new IllegalArgumentException("This SqlParameterSource is empty"); } @@ -49,14 +48,12 @@ public int getSqlType(String paramName) { } @Override - @Nullable - public String getTypeName(String paramName) { + public @Nullable String getTypeName(String paramName) { return null; } @Override - @Nullable - public String[] getParameterNames() { + public String @Nullable [] getParameterNames() { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java index e3502ac63de6..92ab173f4f14 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/MapSqlParameterSource.java @@ -20,9 +20,9 @@ import java.util.LinkedHashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.SqlParameterValue; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -165,8 +165,7 @@ public boolean hasValue(String paramName) { } @Override - @Nullable - public Object getValue(String paramName) { + public @Nullable Object getValue(String paramName) { if (!hasValue(paramName)) { throw new IllegalArgumentException("No value registered for key '" + paramName + "'"); } @@ -174,7 +173,6 @@ public Object getValue(String paramName) { } @Override - @NonNull public String[] getParameterNames() { return StringUtils.toStringArray(this.values.keySet()); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java index 5de5917194fd..ca0503d468d8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcDaoSupport.java @@ -16,9 +16,10 @@ package org.springframework.jdbc.core.namedparam; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.support.JdbcDaoSupport; -import org.springframework.lang.Nullable; /** * Extension of JdbcDaoSupport that exposes a NamedParameterJdbcTemplate as well. @@ -30,8 +31,7 @@ */ public class NamedParameterJdbcDaoSupport extends JdbcDaoSupport { - @Nullable - private NamedParameterJdbcTemplate namedParameterJdbcTemplate; + private @Nullable NamedParameterJdbcTemplate namedParameterJdbcTemplate; /** @@ -48,8 +48,7 @@ protected void initTemplateConfig() { /** * Return a NamedParameterJdbcTemplate wrapping the configured JdbcTemplate. */ - @Nullable - public NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { + public @Nullable NamedParameterJdbcTemplate getNamedParameterJdbcTemplate() { return this.namedParameterJdbcTemplate; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java index f594c5e536f7..b335ee3d259a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcOperations.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.PreparedStatementCallback; @@ -28,7 +30,6 @@ import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.lang.Nullable; /** * Interface specifying a basic set of JDBC operations allowing the use @@ -75,8 +76,7 @@ public interface NamedParameterJdbcOperations { * @return a result object returned by the action, or {@code null} * @throws DataAccessException if there is any problem */ - @Nullable - T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) + @Nullable T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) throws DataAccessException; /** @@ -94,8 +94,7 @@ T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallb * @return a result object returned by the action, or {@code null} * @throws DataAccessException if there is any problem */ - @Nullable - T execute(String sql, Map paramMap, PreparedStatementCallback action) + @Nullable T execute(String sql, Map paramMap, PreparedStatementCallback action) throws DataAccessException; /** @@ -111,8 +110,7 @@ T execute(String sql, Map paramMap, PreparedStatementCallback * @return a result object returned by the action, or {@code null} * @throws DataAccessException if there is any problem */ - @Nullable - T execute(String sql, PreparedStatementCallback action) throws DataAccessException; + @Nullable T execute(String sql, PreparedStatementCallback action) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list @@ -124,8 +122,7 @@ T execute(String sql, Map paramMap, PreparedStatementCallback * @return an arbitrary result object, as returned by the ResultSetExtractor * @throws DataAccessException if the query fails */ - @Nullable - T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) + @Nullable T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) throws DataAccessException; /** @@ -139,8 +136,7 @@ T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rs * @return an arbitrary result object, as returned by the ResultSetExtractor * @throws DataAccessException if the query fails */ - @Nullable - T query(String sql, Map paramMap, ResultSetExtractor rse) + @Nullable T query(String sql, Map paramMap, ResultSetExtractor rse) throws DataAccessException; /** @@ -154,8 +150,7 @@ T query(String sql, Map paramMap, ResultSetExtractor rse) * @return an arbitrary result object, as returned by the ResultSetExtractor * @throws DataAccessException if the query fails */ - @Nullable - T query(String sql, ResultSetExtractor rse) throws DataAccessException; + @Nullable T query(String sql, ResultSetExtractor rse) throws DataAccessException; /** * Query given SQL to create a prepared statement from SQL and a list of @@ -272,13 +267,12 @@ Stream queryForStream(String sql, Map paramMap, RowMapper r * @param paramSource container of arguments to bind to the query * @param rowMapper object that will map one object per row * @return the single mapped object (may be {@code null} if the given - * {@link RowMapper} returned {@code} null) + * {@link RowMapper} returned {@code null}) * @throws org.springframework.dao.IncorrectResultSizeDataAccessException * if the query does not return exactly one row * @throws DataAccessException if the query fails */ - @Nullable - T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + @Nullable T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) throws DataAccessException; /** @@ -290,13 +284,12 @@ T queryForObject(String sql, SqlParameterSource paramSource, RowMapper ro * (leaving it to the PreparedStatement to guess the corresponding SQL type) * @param rowMapper object that will map one object per row * @return the single mapped object (may be {@code null} if the given - * {@link RowMapper} returned {@code} null) + * {@link RowMapper} returned {@code null}) * @throws org.springframework.dao.IncorrectResultSizeDataAccessException * if the query does not return exactly one row * @throws DataAccessException if the query fails */ - @Nullable - T queryForObject(String sql, Map paramMap, RowMapper rowMapper) + @Nullable T queryForObject(String sql, Map paramMap, RowMapper rowMapper) throws DataAccessException; /** @@ -316,8 +309,7 @@ T queryForObject(String sql, Map paramMap, RowMapper rowMapper * @see org.springframework.jdbc.core.JdbcTemplate#queryForObject(String, Class) * @see org.springframework.jdbc.core.SingleColumnRowMapper */ - @Nullable - T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) + @Nullable T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) throws DataAccessException; /** @@ -337,8 +329,7 @@ T queryForObject(String sql, SqlParameterSource paramSource, Class requir * @throws DataAccessException if the query fails * @see org.springframework.jdbc.core.JdbcTemplate#queryForObject(String, Class) */ - @Nullable - T queryForObject(String sql, Map paramMap, Class requiredType) + @Nullable T queryForObject(String sql, Map paramMap, Class requiredType) throws DataAccessException; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java index cba5c9bda0bd..2a9e94892946 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterJdbcTemplate.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. @@ -25,6 +25,8 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.BatchPreparedStatementSetter; @@ -42,7 +44,6 @@ import org.springframework.jdbc.core.SqlRowSetResultSetExtractor; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentLruCache; @@ -149,46 +150,40 @@ public int getCacheLimit() { @Override - @Nullable - public T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) + public @Nullable T execute(String sql, SqlParameterSource paramSource, PreparedStatementCallback action) throws DataAccessException { return getJdbcOperations().execute(getPreparedStatementCreator(sql, paramSource), action); } @Override - @Nullable - public T execute(String sql, Map paramMap, PreparedStatementCallback action) + public @Nullable T execute(String sql, Map paramMap, PreparedStatementCallback action) throws DataAccessException { return execute(sql, new MapSqlParameterSource(paramMap), action); } @Override - @Nullable - public T execute(String sql, PreparedStatementCallback action) throws DataAccessException { + public @Nullable T execute(String sql, PreparedStatementCallback action) throws DataAccessException { return execute(sql, EmptySqlParameterSource.INSTANCE, action); } @Override - @Nullable - public T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) + public @Nullable T query(String sql, SqlParameterSource paramSource, ResultSetExtractor rse) throws DataAccessException { return getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rse); } @Override - @Nullable - public T query(String sql, Map paramMap, ResultSetExtractor rse) + public @Nullable T query(String sql, Map paramMap, ResultSetExtractor rse) throws DataAccessException { return query(sql, new MapSqlParameterSource(paramMap), rse); } @Override - @Nullable - public T query(String sql, ResultSetExtractor rse) throws DataAccessException { + public @Nullable T query(String sql, ResultSetExtractor rse) throws DataAccessException { return query(sql, EmptySqlParameterSource.INSTANCE, rse); } @@ -245,8 +240,7 @@ public Stream queryForStream(String sql, Map paramMap, RowMapp } @Override - @Nullable - public T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) + public @Nullable T queryForObject(String sql, SqlParameterSource paramSource, RowMapper rowMapper) throws DataAccessException { List results = getJdbcOperations().query(getPreparedStatementCreator(sql, paramSource), rowMapper); @@ -254,24 +248,21 @@ public T queryForObject(String sql, SqlParameterSource paramSource, RowMappe } @Override - @Nullable - public T queryForObject(String sql, Map paramMap, RowMapperrowMapper) + public @Nullable T queryForObject(String sql, Map paramMap, RowMapperrowMapper) throws DataAccessException { return queryForObject(sql, new MapSqlParameterSource(paramMap), rowMapper); } @Override - @Nullable - public T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) + public @Nullable T queryForObject(String sql, SqlParameterSource paramSource, Class requiredType) throws DataAccessException { return queryForObject(sql, paramSource, new SingleColumnRowMapper<>(requiredType)); } @Override - @Nullable - public T queryForObject(String sql, Map paramMap, Class requiredType) + public @Nullable T queryForObject(String sql, Map paramMap, Class requiredType) throws DataAccessException { return queryForObject(sql, paramMap, new SingleColumnRowMapper<>(requiredType)); @@ -351,7 +342,7 @@ public int update(String sql, SqlParameterSource paramSource, KeyHolder generate @Override public int update( - String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, @Nullable String[] keyColumnNames) + String sql, SqlParameterSource paramSource, KeyHolder generatedKeyHolder, String @Nullable [] keyColumnNames) throws DataAccessException { PreparedStatementCreator psc = getPreparedStatementCreator(sql, paramSource, pscf -> { @@ -379,7 +370,7 @@ public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs) { new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { - Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); + @Nullable Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); pscf.newPreparedStatementSetter(values).setValues(ps); } @Override @@ -401,7 +392,7 @@ public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder g @Override public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder generatedKeyHolder, - @Nullable String[] keyColumnNames) { + String @Nullable [] keyColumnNames) { if (batchArgs.length == 0) { return new int[0]; @@ -416,12 +407,12 @@ public int[] batchUpdate(String sql, SqlParameterSource[] batchArgs, KeyHolder g else { pscf.setReturnGeneratedKeys(true); } - Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + @Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); PreparedStatementCreator psc = pscf.newPreparedStatementCreator(params); return getJdbcOperations().batchUpdate(psc, new BatchPreparedStatementSetter() { @Override public void setValues(PreparedStatement ps, int i) throws SQLException { - Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); + @Nullable Object[] values = NamedParameterUtils.buildValueArray(parsedSql, batchArgs[i], null); pscf.newPreparedStatementSetter(values).setValues(ps); } @@ -469,7 +460,7 @@ protected PreparedStatementCreator getPreparedStatementCreator(String sql, SqlPa if (customizer != null) { customizer.accept(pscf); } - Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); + @Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, null); return pscf.newPreparedStatementCreator(params); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java index 4d9f1141dfa3..6df168ae65d8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/NamedParameterUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/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. @@ -22,10 +22,11 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.SqlParameterValue; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -347,10 +348,10 @@ public static String substituteNamedParameters(ParsedSql parsedSql, @Nullable Sq * be built into the value array in the form of SqlParameterValue objects. * @return the array of values */ - public static Object[] buildValueArray( + public static @Nullable Object[] buildValueArray( ParsedSql parsedSql, SqlParameterSource paramSource, @Nullable List declaredParams) { - Object[] paramArray = new Object[parsedSql.getTotalParameterCount()]; + @Nullable Object[] paramArray = new Object[parsedSql.getTotalParameterCount()]; if (parsedSql.getNamedParameterCount() > 0 && parsedSql.getUnnamedParameterCount() > 0) { throw new InvalidDataAccessApiUsageException( "Not allowed to mix named and traditional ? placeholders. You have " + @@ -387,8 +388,7 @@ public static Object[] buildValueArray( * @param paramIndex the index of the desired parameter * @return the declared SqlParameter, or {@code null} if none found */ - @Nullable - private static SqlParameter findParameter( + private static @Nullable SqlParameter findParameter( @Nullable List declaredParams, String paramName, int paramIndex) { if (declaredParams != null) { @@ -497,7 +497,7 @@ public static String substituteNamedParameters(String sql, SqlParameterSource pa * @param paramMap the Map of parameters * @return the array of values */ - public static Object[] buildValueArray(String sql, Map paramMap) { + public static @Nullable Object[] buildValueArray(String sql, Map paramMap) { ParsedSql parsedSql = parseSqlStatement(sql); return buildValueArray(parsedSql, new MapSqlParameterSource(paramMap), null); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java index 60583679a725..ca018cfd6d32 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SimplePropertySqlParameterSource.java @@ -21,9 +21,10 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.jdbc.core.StatementCreatorUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -72,8 +73,7 @@ public boolean hasValue(String paramName) { } @Override - @Nullable - public Object getValue(String paramName) throws IllegalArgumentException { + public @Nullable Object getValue(String paramName) throws IllegalArgumentException { Object desc = getDescriptor(paramName); if (desc instanceof PropertyDescriptor pd) { ReflectionUtils.makeAccessible(pd.getReadMethod()); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java index 63747b25284e..48340f4abd69 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSource.java @@ -16,8 +16,9 @@ package org.springframework.jdbc.core.namedparam; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.support.JdbcUtils; -import org.springframework.lang.Nullable; /** * Interface that defines common functionality for objects that can @@ -63,8 +64,7 @@ public interface SqlParameterSource { * @return the value of the specified parameter * @throws IllegalArgumentException if there is no value for the requested parameter */ - @Nullable - Object getValue(String paramName) throws IllegalArgumentException; + @Nullable Object getValue(String paramName) throws IllegalArgumentException; /** * Determine the SQL type for the specified named parameter. @@ -83,8 +83,7 @@ default int getSqlType(String paramName) { * @return the type name of the specified parameter, * or {@code null} if not known */ - @Nullable - default String getTypeName(String paramName) { + default @Nullable String getTypeName(String paramName) { return null; } @@ -97,8 +96,7 @@ default String getTypeName(String paramName) { * @since 5.0.3 * @see SqlParameterSourceUtils#extractCaseInsensitiveParameterNames */ - @Nullable - default String[] getParameterNames() { + default String @Nullable [] getParameterNames() { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java index 9709561eb286..81c303c8ee5e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/SqlParameterSourceUtils.java @@ -22,8 +22,9 @@ import java.util.Locale; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.SqlParameterValue; -import org.springframework.lang.Nullable; /** * Class that provides helper methods for the use of {@link SqlParameterSource}, @@ -95,8 +96,7 @@ public static SqlParameterSource[] createBatch(Map[] valueMaps) { * @return the value object * @see SqlParameterValue */ - @Nullable - public static Object getTypedValue(SqlParameterSource source, String parameterName) { + public static @Nullable Object getTypedValue(SqlParameterSource source, String parameterName) { int sqlType = source.getSqlType(parameterName); if (sqlType != SqlParameterSource.TYPE_UNKNOWN) { return new SqlParameterValue(sqlType, source.getTypeName(parameterName), source.getValue(parameterName)); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java index d3fc2928d6d4..1b4a01787c13 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/namedparam/package-info.java @@ -10,9 +10,7 @@ * the {@code getJdbcOperations()} method of NamedParameterJdbcTemplate and * work with the returned classic template, or use a JdbcTemplate instance directly. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.core.namedparam; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java index 2d79acddb044..0d0e604d4e08 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/package-info.java @@ -2,9 +2,7 @@ * Provides the core JDBC framework, based on JdbcTemplate * and its associated callback interfaces and helper objects. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.core; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java index 71d8589a1ee1..e41f625ec51f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcCall.java @@ -28,6 +28,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.CallableStatementCreator; @@ -37,7 +38,6 @@ import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.metadata.CallMetaDataContext; import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -78,15 +78,13 @@ public abstract class AbstractJdbcCall { private volatile boolean compiled; /** The generated string used for call statement. */ - @Nullable - private String callString; + private @Nullable String callString; /** * A delegate enabling us to create CallableStatementCreators * efficiently, based on this class's declared parameters. */ - @Nullable - private CallableStatementCreatorFactory callableStatementFactory; + private @Nullable CallableStatementCreatorFactory callableStatementFactory; /** @@ -124,8 +122,7 @@ public void setProcedureName(@Nullable String procedureName) { /** * Get the name of the stored procedure. */ - @Nullable - public String getProcedureName() { + public @Nullable String getProcedureName() { return this.callMetaDataContext.getProcedureName(); } @@ -153,8 +150,7 @@ public void setCatalogName(@Nullable String catalogName) { /** * Get the catalog name used. */ - @Nullable - public String getCatalogName() { + public @Nullable String getCatalogName() { return this.callMetaDataContext.getCatalogName(); } @@ -168,8 +164,7 @@ public void setSchemaName(@Nullable String schemaName) { /** * Get the schema name used. */ - @Nullable - public String getSchemaName() { + public @Nullable String getSchemaName() { return this.callMetaDataContext.getSchemaName(); } @@ -231,8 +226,7 @@ public void setAccessCallParameterMetaData(boolean accessCallParameterMetaData) /** * Get the call string that should be used based on parameters and meta-data. */ - @Nullable - public String getCallString() { + public @Nullable String getCallString() { return this.callString; } @@ -254,6 +248,10 @@ protected CallableStatementCreatorFactory getCallableStatementFactory() { * @param parameter the {@link SqlParameter} to add */ public void addDeclaredParameter(SqlParameter parameter) { + if (isCompiled()) { + throw new IllegalStateException("SqlCall for " + (isFunction() ? "function" : "procedure") + + " is already compiled"); + } Assert.notNull(parameter, "The supplied parameter must not be null"); if (!StringUtils.hasText(parameter.getName())) { throw new InvalidDataAccessApiUsageException( @@ -271,6 +269,10 @@ public void addDeclaredParameter(SqlParameter parameter) { * @param rowMapper the RowMapper implementation to use */ public void addDeclaredRowMapper(String parameterName, RowMapper rowMapper) { + if (isCompiled()) { + throw new IllegalStateException("SqlCall for " + (isFunction() ? "function" : "procedure") + + " is already compiled"); + } this.declaredRowMappers.put(parameterName, rowMapper); if (logger.isDebugEnabled()) { logger.debug("Added row mapper for [" + getProcedureName() + "]: " + parameterName); @@ -428,8 +430,7 @@ private Map executeCallInternal(Map args) { * Get the name of a single out parameter or return value. * Used for functions or procedures with one out parameter. */ - @Nullable - protected String getScalarOutParameterName() { + protected @Nullable String getScalarOutParameterName() { return this.callMetaDataContext.getScalarOutParameterName(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java index 6b86dbb67390..fbda7f1a3217 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/AbstractJdbcInsert.java @@ -33,6 +33,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.InvalidDataAccessApiUsageException; @@ -47,7 +48,6 @@ import org.springframework.jdbc.support.GeneratedKeyHolder; import org.springframework.jdbc.support.JdbcUtils; import org.springframework.jdbc.support.KeyHolder; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -131,8 +131,7 @@ public void setTableName(@Nullable String tableName) { /** * Get the name of the table for this insert. */ - @Nullable - public String getTableName() { + public @Nullable String getTableName() { return this.tableMetaDataContext.getTableName(); } @@ -147,8 +146,7 @@ public void setSchemaName(@Nullable String schemaName) { /** * Get the name of the schema for this insert. */ - @Nullable - public String getSchemaName() { + public @Nullable String getSchemaName() { return this.tableMetaDataContext.getSchemaName(); } @@ -163,8 +161,7 @@ public void setCatalogName(@Nullable String catalogName) { /** * Get the name of the catalog for this insert. */ - @Nullable - public String getCatalogName() { + public @Nullable String getCatalogName() { return this.tableMetaDataContext.getCatalogName(); } @@ -616,7 +613,7 @@ public int getBatchSize() { * @param preparedStatement the PreparedStatement * @param values the values to be set */ - private void setParameterValues(PreparedStatement preparedStatement, List values, @Nullable int... columnTypes) + private void setParameterValues(PreparedStatement preparedStatement, List values, int @Nullable ... columnTypes) throws SQLException { int colIndex = 0; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java index 7ef7d6ccfc0a..ea6630c79d12 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/DefaultJdbcClient.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. @@ -25,7 +25,11 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; +import org.springframework.core.convert.ConversionService; +import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCreator; @@ -43,7 +47,6 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,24 +66,25 @@ final class DefaultJdbcClient implements JdbcClient { private final NamedParameterJdbcOperations namedParamOps; + private final ConversionService conversionService; + private final Map, RowMapper> rowMapperCache = new ConcurrentHashMap<>(); public DefaultJdbcClient(DataSource dataSource) { - this.classicOps = new JdbcTemplate(dataSource); - this.namedParamOps = new NamedParameterJdbcTemplate(this.classicOps); + this(new JdbcTemplate(dataSource)); } public DefaultJdbcClient(JdbcOperations jdbcTemplate) { - Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); - this.classicOps = jdbcTemplate; - this.namedParamOps = new NamedParameterJdbcTemplate(jdbcTemplate); + this(new NamedParameterJdbcTemplate(jdbcTemplate), null); } - public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate) { + public DefaultJdbcClient(NamedParameterJdbcOperations jdbcTemplate, @Nullable ConversionService conversionService) { Assert.notNull(jdbcTemplate, "JdbcTemplate must not be null"); this.classicOps = jdbcTemplate.getJdbcOperations(); this.namedParamOps = jdbcTemplate; + this.conversionService = + (conversionService != null ? conversionService : DefaultConversionService.getSharedInstance()); } @@ -200,8 +204,9 @@ public ResultQuerySpec query() { @Override public MappedQuerySpec query(Class mappedClass) { RowMapper rowMapper = rowMapperCache.computeIfAbsent(mappedClass, key -> - BeanUtils.isSimpleProperty(mappedClass) ? new SingleColumnRowMapper<>(mappedClass) : - new SimplePropertyRowMapper<>(mappedClass)); + BeanUtils.isSimpleProperty(mappedClass) ? + new SingleColumnRowMapper<>(mappedClass, conversionService) : + new SimplePropertyRowMapper<>(mappedClass, conversionService)); return query((RowMapper) rowMapper); } @@ -268,7 +273,7 @@ private PreparedStatementCreator statementCreatorForIndexedParams() { return new PreparedStatementCreatorFactory(this.sql).newPreparedStatementCreator(this.indexedParams); } - private PreparedStatementCreator statementCreatorForIndexedParamsWithKeys(@Nullable String[] keyColumnNames) { + private PreparedStatementCreator statementCreatorForIndexedParamsWithKeys(String @Nullable [] keyColumnNames) { PreparedStatementCreatorFactory pscf = new PreparedStatementCreatorFactory(this.sql); if (keyColumnNames != null) { pscf.setGeneratedKeysColumnNames(keyColumnNames); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java index 5f80796b1aa8..1fae8141d40d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/JdbcClient.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,9 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + +import org.springframework.core.convert.ConversionService; import org.springframework.dao.support.DataAccessUtils; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.ResultSetExtractor; @@ -35,7 +38,6 @@ import org.springframework.jdbc.core.namedparam.SqlParameterSource; import org.springframework.jdbc.support.KeyHolder; import org.springframework.jdbc.support.rowset.SqlRowSet; -import org.springframework.lang.Nullable; /** * A fluent {@code JdbcClient} with common JDBC query and update operations, @@ -106,7 +108,22 @@ static JdbcClient create(JdbcOperations jdbcTemplate) { * @param jdbcTemplate the delegate to perform operations on */ static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate) { - return new DefaultJdbcClient(jdbcTemplate); + return new DefaultJdbcClient(jdbcTemplate, null); + } + + /** + * Create a {@code JdbcClient} for the given {@link NamedParameterJdbcOperations} delegate, + * typically an {@link org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate}. + *

    Use this factory method to reuse existing {@code NamedParameterJdbcTemplate} + * configuration, including its underlying {@code JdbcTemplate} and {@code DataSource}, + * along with a custom {@link ConversionService} for queries with mapped classes. + * @param jdbcTemplate the delegate to perform operations on + * @param conversionService a {@link ConversionService} for converting fetched JDBC values + * to mapped classes in {@link StatementSpec#query(Class)} + * @since 7.0 + */ + static JdbcClient create(NamedParameterJdbcOperations jdbcTemplate, ConversionService conversionService) { + return new DefaultJdbcClient(jdbcTemplate, conversionService); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java index 6331423200b4..ffcdcfc6f9a1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCall.java @@ -22,11 +22,12 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.lang.Nullable; /** * A SimpleJdbcCall is a multithreaded, reusable object representing a call @@ -149,44 +150,38 @@ public SimpleJdbcCall withNamedBinding() { } @Override - @Nullable @SuppressWarnings("unchecked") - public T executeFunction(Class returnType, Object... args) { + public @Nullable T executeFunction(Class returnType, Object... args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override - @Nullable @SuppressWarnings("unchecked") - public T executeFunction(Class returnType, Map args) { + public @Nullable T executeFunction(Class returnType, Map args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override - @Nullable @SuppressWarnings("unchecked") - public T executeFunction(Class returnType, SqlParameterSource args) { + public @Nullable T executeFunction(Class returnType, SqlParameterSource args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override - @Nullable @SuppressWarnings("unchecked") - public T executeObject(Class returnType, Object... args) { + public @Nullable T executeObject(Class returnType, Object... args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override - @Nullable @SuppressWarnings("unchecked") - public T executeObject(Class returnType, Map args) { + public @Nullable T executeObject(Class returnType, Map args) { return (T) doExecute(args).get(getScalarOutParameterName()); } @Override - @Nullable @SuppressWarnings("unchecked") - public T executeObject(Class returnType, SqlParameterSource args) { + public @Nullable T executeObject(Class returnType, SqlParameterSource args) { return (T) doExecute(args).get(getScalarOutParameterName()); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java index 91db9847925d..ea784a403892 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/SimpleJdbcCallOperations.java @@ -18,10 +18,11 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.namedparam.SqlParameterSource; -import org.springframework.lang.Nullable; /** * Interface specifying the API for a Simple JDBC Call implemented by {@link SimpleJdbcCall}. @@ -118,8 +119,7 @@ public interface SimpleJdbcCallOperations { * Parameter values must be provided in the same order as the parameters are defined * for the stored procedure. */ - @Nullable - T executeFunction(Class returnType, Object... args); + @Nullable T executeFunction(Class returnType, Object... args); /** * Execute the stored function and return the results obtained as an Object of the @@ -127,8 +127,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args a Map containing the parameter values to be used in the call */ - @Nullable - T executeFunction(Class returnType, Map args); + @Nullable T executeFunction(Class returnType, Map args); /** * Execute the stored function and return the results obtained as an Object of the @@ -136,8 +135,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args the MapSqlParameterSource containing the parameter values to be used in the call */ - @Nullable - T executeFunction(Class returnType, SqlParameterSource args); + @Nullable T executeFunction(Class returnType, SqlParameterSource args); /** * Execute the stored procedure and return the single out parameter as an Object @@ -148,8 +146,7 @@ public interface SimpleJdbcCallOperations { * Parameter values must be provided in the same order as the parameters are defined for * the stored procedure. */ - @Nullable - T executeObject(Class returnType, Object... args); + @Nullable T executeObject(Class returnType, Object... args); /** * Execute the stored procedure and return the single out parameter as an Object @@ -158,8 +155,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args a Map containing the parameter values to be used in the call */ - @Nullable - T executeObject(Class returnType, Map args); + @Nullable T executeObject(Class returnType, Map args); /** * Execute the stored procedure and return the single out parameter as an Object @@ -168,8 +164,7 @@ public interface SimpleJdbcCallOperations { * @param returnType the type of the value to return * @param args the MapSqlParameterSource containing the parameter values to be used in the call */ - @Nullable - T executeObject(Class returnType, SqlParameterSource args); + @Nullable T executeObject(Class returnType, SqlParameterSource args); /** * Execute the stored procedure and return a map of output params, keyed by name diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java index d8a3bb7e5311..59c640dc1166 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/simple/package-info.java @@ -8,9 +8,7 @@ * meta-data provided by the JDBC driver to simplify the application code. Much of the * parameter specification becomes unnecessary since it can be looked up in the meta-data. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.core.simple; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java index c56e4a2811f9..a6d8516df472 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractLobStreamingResultSetExtractor.java @@ -20,12 +20,13 @@ import java.sql.ResultSet; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.dao.IncorrectResultSizeDataAccessException; import org.springframework.jdbc.LobRetrievalFailureException; import org.springframework.jdbc.core.ResultSetExtractor; -import org.springframework.lang.Nullable; /** * Abstract ResultSetExtractor implementation that assumes streaming of LOB data. @@ -70,8 +71,7 @@ public abstract class AbstractLobStreamingResultSetExtractor implements Resul * @see org.springframework.jdbc.LobRetrievalFailureException */ @Override - @Nullable - public final T extractData(ResultSet rs) throws SQLException, DataAccessException { + public final @Nullable T extractData(ResultSet rs) throws SQLException, DataAccessException { if (!rs.next()) { handleNoRowFound(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java index 28e10a985255..fb04aeb08316 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/AbstractSqlTypeValue.java @@ -20,8 +20,9 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.SqlTypeValue; -import org.springframework.lang.Nullable; /** * Abstract implementation of the SqlTypeValue interface, for convenient 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 9f932a5cfde0..e0814b44c22c 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. @@ -20,9 +20,10 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -41,16 +42,15 @@ * @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; - @Nullable - private JdbcTemplate jdbcTemplate; + private @Nullable JdbcTemplate jdbcTemplate; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java index f9a5ceb934a4..5ff1f765f7f7 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/JdbcDaoSupport.java @@ -20,12 +20,13 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.support.DaoSupport; import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.support.SQLExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -47,8 +48,7 @@ */ public abstract class JdbcDaoSupport extends DaoSupport { - @Nullable - private JdbcTemplate jdbcTemplate; + private @Nullable JdbcTemplate jdbcTemplate; /** @@ -77,8 +77,7 @@ protected JdbcTemplate createJdbcTemplate(DataSource dataSource) { /** * Return the JDBC DataSource used by this DAO. */ - @Nullable - public final DataSource getDataSource() { + public final @Nullable DataSource getDataSource() { return (this.jdbcTemplate != null ? this.jdbcTemplate.getDataSource() : null); } @@ -95,8 +94,7 @@ public final void setJdbcTemplate(@Nullable JdbcTemplate jdbcTemplate) { * Return the JdbcTemplate for this DAO, * pre-initialized with the DataSource or set explicitly. */ - @Nullable - public final JdbcTemplate getJdbcTemplate() { + public final @Nullable JdbcTemplate getJdbcTemplate() { return this.jdbcTemplate; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java index 08a9f3019c46..7f113eb5f6e4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlBinaryValue.java @@ -23,10 +23,11 @@ import java.sql.SQLException; import java.sql.Types; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.InputStreamSource; import org.springframework.core.io.Resource; import org.springframework.jdbc.core.SqlTypeValue; -import org.springframework.lang.Nullable; /** * Object to represent a binary parameter value for a SQL statement, for example, diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java index aaaf80d79835..6ef5e04fa252 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlCharacterValue.java @@ -24,8 +24,9 @@ import java.sql.SQLException; import java.sql.Types; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.SqlTypeValue; -import org.springframework.lang.Nullable; /** * Object to represent a character-based parameter value for a SQL statement, diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java index c208be0e3f40..15bd704aaf4c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/SqlLobValue.java @@ -22,11 +22,12 @@ import java.sql.SQLException; import java.sql.Types; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.DisposableSqlTypeValue; import org.springframework.jdbc.support.lob.DefaultLobHandler; import org.springframework.jdbc.support.lob.LobCreator; import org.springframework.jdbc.support.lob.LobHandler; -import org.springframework.lang.Nullable; /** * Object to represent an SQL BLOB/CLOB value parameter. BLOBs can either be an @@ -73,8 +74,7 @@ @Deprecated(since = "6.2") public class SqlLobValue implements DisposableSqlTypeValue { - @Nullable - private final Object content; + private final @Nullable Object content; private final int length; @@ -90,7 +90,7 @@ public class SqlLobValue implements DisposableSqlTypeValue { * @param bytes the byte array containing the BLOB value * @see org.springframework.jdbc.support.lob.DefaultLobHandler */ - public SqlLobValue(@Nullable byte[] bytes) { + public SqlLobValue(byte @Nullable [] bytes) { this(bytes, new DefaultLobHandler()); } @@ -99,7 +99,7 @@ public SqlLobValue(@Nullable byte[] bytes) { * @param bytes the byte array containing the BLOB value * @param lobHandler the LobHandler to be used */ - public SqlLobValue(@Nullable byte[] bytes, LobHandler lobHandler) { + public SqlLobValue(byte @Nullable [] bytes, LobHandler lobHandler) { this.content = bytes; this.length = (bytes != null ? bytes.length : 0); this.lobCreator = lobHandler.getLobCreator(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java index 7e0621e7740b..b8969eabd1b1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/core/support/package-info.java @@ -2,9 +2,7 @@ * Classes supporting the {@code org.springframework.jdbc.core} package. * Contains a DAO base class for JdbcTemplate usage. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.core.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java index df6df3e6278f..075d983f497e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/AbstractDriverBasedDataSource.java @@ -20,7 +20,7 @@ import java.sql.SQLException; import java.util.Properties; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract base class for JDBC {@link javax.sql.DataSource} implementations @@ -33,23 +33,17 @@ */ public abstract class AbstractDriverBasedDataSource extends AbstractDataSource { - @Nullable - private String url; + private @Nullable String url; - @Nullable - private String username; + private @Nullable String username; - @Nullable - private String password; + private @Nullable String password; - @Nullable - private String catalog; + private @Nullable String catalog; - @Nullable - private String schema; + private @Nullable String schema; - @Nullable - private Properties connectionProperties; + private @Nullable Properties connectionProperties; /** @@ -63,8 +57,7 @@ public void setUrl(@Nullable String url) { /** * Return the JDBC URL to use for connecting through the Driver. */ - @Nullable - public String getUrl() { + public @Nullable String getUrl() { return this.url; } @@ -79,8 +72,7 @@ public void setUsername(@Nullable String username) { /** * Return the JDBC username to use for connecting through the Driver. */ - @Nullable - public String getUsername() { + public @Nullable String getUsername() { return this.username; } @@ -95,8 +87,7 @@ public void setPassword(@Nullable String password) { /** * Return the JDBC password to use for connecting through the Driver. */ - @Nullable - public String getPassword() { + public @Nullable String getPassword() { return this.password; } @@ -113,8 +104,7 @@ public void setCatalog(@Nullable String catalog) { * Return the database catalog to be applied to each Connection, if any. * @since 4.3.2 */ - @Nullable - public String getCatalog() { + public @Nullable String getCatalog() { return this.catalog; } @@ -131,8 +121,7 @@ public void setSchema(@Nullable String schema) { * Return the database schema to be applied to each Connection, if any. * @since 4.3.2 */ - @Nullable - public String getSchema() { + public @Nullable String getSchema() { return this.schema; } @@ -151,8 +140,7 @@ public void setConnectionProperties(@Nullable Properties connectionProperties) { /** * Return the connection properties to be passed to the Driver, if any. */ - @Nullable - public Properties getConnectionProperties() { + public @Nullable Properties getConnectionProperties() { return this.connectionProperties; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java index c8f01712c825..644036065ed1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ConnectionHolder.java @@ -20,7 +20,8 @@ import java.sql.SQLException; import java.sql.Savepoint; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -47,16 +48,13 @@ public class ConnectionHolder extends ResourceHolderSupport { public static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_"; - @Nullable - private ConnectionHandle connectionHandle; + private @Nullable ConnectionHandle connectionHandle; - @Nullable - private Connection currentConnection; + private @Nullable Connection currentConnection; private boolean transactionActive = false; - @Nullable - private Boolean savepointsSupported; + private @Nullable Boolean savepointsSupported; private int savepointCounter = 0; @@ -99,8 +97,7 @@ public ConnectionHolder(Connection connection, boolean transactionActive) { /** * Return the ConnectionHandle held by this ConnectionHolder. */ - @Nullable - public ConnectionHandle getConnectionHandle() { + public @Nullable ConnectionHandle getConnectionHandle() { return this.connectionHandle; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java index 49d111d3d6bf..6069e2016b2d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DataSourceTransactionManager.java @@ -22,8 +22,9 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionSystemException; @@ -121,8 +122,7 @@ public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; private boolean enforceReadOnly = false; @@ -180,8 +180,7 @@ public void setDataSource(@Nullable DataSource dataSource) { /** * Return the JDBC {@code DataSource} that this instance manages transactions for. */ - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { return this.dataSource; } 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 8daa652a3d15..64be10894b9d 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 @@ -24,9 +24,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -174,8 +174,7 @@ private static Connection fetchConnection(DataSource dataSource) throws SQLExcep * @see Connection#setTransactionIsolation * @see Connection#setReadOnly */ - @Nullable - public static Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) + public static @Nullable Integer prepareConnectionForTransaction(Connection con, @Nullable TransactionDefinition definition) throws SQLException { Assert.notNull(con, "No Connection specified"); @@ -264,10 +263,9 @@ public static void resetConnectionAfterTransaction( * 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/DelegatingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java index fccd2b69b643..d99de4f80275 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/DelegatingDataSource.java @@ -25,8 +25,9 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,8 +44,7 @@ */ public class DelegatingDataSource implements DataSource, InitializingBean { - @Nullable - private DataSource targetDataSource; + private @Nullable DataSource targetDataSource; /** @@ -73,8 +73,7 @@ public void setTargetDataSource(@Nullable DataSource targetDataSource) { /** * Return the target DataSource that this DataSource should delegate to. */ - @Nullable - public DataSource getTargetDataSource() { + public @Nullable DataSource getTargetDataSource() { return this.targetDataSource; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java index 86a7b91de7ea..6a624e47081d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/IsolationLevelDataSourceAdapter.java @@ -20,7 +20,8 @@ import java.sql.SQLException; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -69,8 +70,7 @@ public class IsolationLevelDataSourceAdapter extends UserCredentialsDataSourceAd ); - @Nullable - private Integer isolationLevel; + private @Nullable Integer isolationLevel; /** @@ -122,8 +122,7 @@ public void setIsolationLevel(int isolationLevel) { * Return the statically specified isolation level, * or {@code null} if none. */ - @Nullable - protected Integer getIsolationLevel() { + protected @Nullable Integer getIsolationLevel() { return this.isolationLevel; } @@ -155,8 +154,7 @@ protected Connection doGetConnection(@Nullable String username, @Nullable String * @see org.springframework.transaction.support.TransactionSynchronizationManager#getCurrentTransactionIsolationLevel() * @see #setIsolationLevel */ - @Nullable - protected Integer getCurrentIsolationLevel() { + protected @Nullable Integer getCurrentIsolationLevel() { Integer isolationLevelToUse = TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); if (isolationLevelToUse == null) { isolationLevelToUse = getIsolationLevel(); @@ -170,8 +168,7 @@ protected Integer getCurrentIsolationLevel() { * @return whether there is a read-only hint for the current scope * @see org.springframework.transaction.support.TransactionSynchronizationManager#isCurrentTransactionReadOnly() */ - @Nullable - protected Boolean getCurrentReadOnlyFlag() { + protected @Nullable Boolean getCurrentReadOnlyFlag() { boolean txReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly(); return (txReadOnly ? Boolean.TRUE : null); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java index aaa09e3a691b..4ca14f99a3fe 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/JdbcTransactionObjectSupport.java @@ -20,7 +20,8 @@ import java.sql.SQLFeatureNotSupportedException; import java.sql.Savepoint; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.NestedTransactionNotSupportedException; import org.springframework.transaction.SavepointManager; @@ -46,11 +47,9 @@ */ public abstract class JdbcTransactionObjectSupport implements SavepointManager, SmartTransactionObject { - @Nullable - private ConnectionHolder connectionHolder; + private @Nullable ConnectionHolder connectionHolder; - @Nullable - private Integer previousIsolationLevel; + private @Nullable Integer previousIsolationLevel; private boolean readOnly = false; @@ -89,8 +88,7 @@ public void setPreviousIsolationLevel(@Nullable Integer previousIsolationLevel) /** * Return the retained previous isolation level, if any. */ - @Nullable - public Integer getPreviousIsolationLevel() { + public @Nullable Integer getPreviousIsolationLevel() { return this.previousIsolationLevel; } 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 38cd26a6272f..75269e12accc 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. @@ -29,8 +29,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -77,6 +77,9 @@ * 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} @@ -104,14 +107,11 @@ public class LazyConnectionDataSourceProxy extends DelegatingDataSource { private static final Log logger = LogFactory.getLog(LazyConnectionDataSourceProxy.class); - @Nullable - private DataSource readOnlyDataSource; + private @Nullable DataSource readOnlyDataSource; - @Nullable - private volatile Boolean defaultAutoCommit; + private volatile @Nullable Boolean defaultAutoCommit; - @Nullable - private volatile Integer defaultTransactionIsolation; + private volatile @Nullable Integer defaultTransactionIsolation; /** @@ -238,16 +238,14 @@ protected void checkDefaultConnectionProperties(Connection con) throws SQLExcept /** * Expose the default auto-commit value. */ - @Nullable - protected Boolean defaultAutoCommit() { + protected @Nullable Boolean defaultAutoCommit() { return this.defaultAutoCommit; } /** * Expose the default transaction isolation value. */ - @Nullable - protected Integer defaultTransactionIsolation() { + protected @Nullable Integer defaultTransactionIsolation() { return this.defaultTransactionIsolation; } @@ -295,17 +293,13 @@ public Connection getConnection(String username, String password) throws SQLExce */ private class LazyConnectionInvocationHandler implements InvocationHandler { - @Nullable - private String username; + private @Nullable String username; - @Nullable - private String password; + private @Nullable String password; - @Nullable - private Boolean autoCommit; + private @Nullable Boolean autoCommit; - @Nullable - private Integer transactionIsolation; + private @Nullable Integer transactionIsolation; private boolean readOnly = false; @@ -313,8 +307,7 @@ private class LazyConnectionInvocationHandler implements InvocationHandler { private boolean closed = false; - @Nullable - private Connection target; + private @Nullable Connection target; public LazyConnectionInvocationHandler() { this.autoCommit = defaultAutoCommit(); @@ -328,8 +321,7 @@ public LazyConnectionInvocationHandler(String username, String password) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ConnectionProxy interface coming in... switch (method.getName()) { @@ -436,11 +428,19 @@ public Object invoke(Object proxy, Method method, Object[] args) throws Throwabl 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/datasource/ShardingKeyDataSourceAdapter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapter.java index 13194ec754c8..73e97529e055 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapter.java @@ -23,7 +23,7 @@ import javax.sql.DataSource; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An adapter for a target {@link DataSource}, designed to apply sharding keys, if specified, @@ -53,8 +53,7 @@ */ public class ShardingKeyDataSourceAdapter extends DelegatingDataSource { - @Nullable - private ShardingKeyProvider shardingkeyProvider; + private @Nullable ShardingKeyProvider shardingkeyProvider; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyProvider.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyProvider.java index f9c99b177f7c..3f868dc19fb3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyProvider.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/ShardingKeyProvider.java @@ -19,7 +19,7 @@ import java.sql.SQLException; import java.sql.ShardingKey; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface for determining sharding keys which are used to establish direct @@ -43,8 +43,7 @@ public interface ShardingKeyProvider { * @return the sharding key, or {@code null} if it is not available or cannot be determined * @throws SQLException if an error occurs while obtaining the sharding key */ - @Nullable - ShardingKey getShardingKey() throws SQLException; + @Nullable ShardingKey getShardingKey() throws SQLException; /** * Determine the super sharding key, if any. This method returns the super sharding key @@ -53,8 +52,7 @@ public interface ShardingKeyProvider { * determined (the default) * @throws SQLException if an error occurs while obtaining the super sharding key */ - @Nullable - default ShardingKey getSuperShardingKey() throws SQLException { + default @Nullable ShardingKey getSuperShardingKey() throws SQLException { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java index 99a00687e470..e81365a540e1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SimpleDriverDataSource.java @@ -21,8 +21,9 @@ import java.sql.SQLException; import java.util.Properties; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -54,8 +55,7 @@ */ public class SimpleDriverDataSource extends AbstractDriverBasedDataSource { - @Nullable - private Driver driver; + private @Nullable Driver driver; /** @@ -127,8 +127,7 @@ public void setDriver(@Nullable Driver driver) { /** * Return the JDBC Driver instance to use. */ - @Nullable - public Driver getDriver() { + public @Nullable Driver getDriver() { return this.driver; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java index a258aa3a21a0..76c9ce9025f9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/SingleConnectionDataSource.java @@ -25,8 +25,9 @@ import java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.DisposableBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -64,16 +65,13 @@ public class SingleConnectionDataSource extends DriverManagerDataSource private boolean rollbackBeforeClose; /** Override auto-commit state? */ - @Nullable - private Boolean autoCommit; + private @Nullable Boolean autoCommit; /** Wrapped Connection. */ - @Nullable - private Connection target; + private @Nullable Connection target; /** Proxy Connection. */ - @Nullable - private Connection connection; + private @Nullable Connection connection; /** Lifecycle lock for the shared Connection. */ private final Lock connectionLock = new ReentrantLock(); @@ -175,14 +173,13 @@ public void setAutoCommit(boolean autoCommit) { * Return whether the returned Connection's "autoCommit" setting should be overridden. * @return the "autoCommit" value, or {@code null} if none to be applied */ - @Nullable - protected Boolean getAutoCommitValue() { + protected @Nullable Boolean getAutoCommitValue() { return this.autoCommit; } @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Connection getConnection() throws SQLException { this.connectionLock.lock(); try { @@ -368,8 +365,7 @@ public CloseSuppressingInvocationHandler(Connection target) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ConnectionProxy interface coming in... return switch (method.getName()) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java index 3945c896eeea..20bbf01eaff4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/TransactionAwareDataSourceProxy.java @@ -26,7 +26,8 @@ import javax.sql.DataSource; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.transaction.support.TransactionSynchronizationManager; /** @@ -180,8 +181,7 @@ private class TransactionAwareInvocationHandler implements InvocationHandler { private final DataSource targetDataSource; - @Nullable - private Connection target; + private @Nullable Connection target; private boolean closed = false; @@ -190,8 +190,7 @@ public TransactionAwareInvocationHandler(DataSource targetDataSource) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on ConnectionProxy interface coming in... switch (method.getName()) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java index 98203528fc41..b47a3ca3818c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/UserCredentialsDataSourceAdapter.java @@ -19,8 +19,9 @@ import java.sql.Connection; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -62,17 +63,13 @@ */ public class UserCredentialsDataSourceAdapter extends DelegatingDataSource { - @Nullable - private String username; + private @Nullable String username; - @Nullable - private String password; + private @Nullable String password; - @Nullable - private String catalog; + private @Nullable String catalog; - @Nullable - private String schema; + private @Nullable String schema; private final ThreadLocal threadBoundCredentials = new NamedThreadLocal<>("Current JDBC user credentials"); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java index 7375d7044e38..f764a3b08cd9 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/DerbyEmbeddedDatabaseConfigurer.java @@ -23,8 +23,7 @@ import org.apache.commons.logging.LogFactory; import org.apache.derby.jdbc.EmbeddedDriver; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link EmbeddedDatabaseConfigurer} for the Apache Derby database. @@ -39,8 +38,7 @@ final class DerbyEmbeddedDatabaseConfigurer implements EmbeddedDatabaseConfigure private static final String URL_TEMPLATE = "jdbc:derby:memory:%s;%s"; - @Nullable - private static DerbyEmbeddedDatabaseConfigurer instance; + private static @Nullable DerbyEmbeddedDatabaseConfigurer instance; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java index 0b08b5955ddb..06fce61af6d0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactory.java @@ -26,11 +26,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jdbc.datasource.SimpleDriverDataSource; import org.springframework.jdbc.datasource.init.DatabasePopulator; import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -80,14 +80,11 @@ public class EmbeddedDatabaseFactory { private DataSourceFactory dataSourceFactory = new SimpleDriverDataSourceFactory(); - @Nullable - private EmbeddedDatabaseConfigurer databaseConfigurer; + private @Nullable EmbeddedDatabaseConfigurer databaseConfigurer; - @Nullable - private DatabasePopulator databasePopulator; + private @Nullable DatabasePopulator databasePopulator; - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; /** @@ -162,7 +159,7 @@ public void setDatabasePopulator(DatabasePopulator populator) { * Factory method that returns the {@linkplain EmbeddedDatabase embedded database} * instance, which is also a {@link DataSource}. */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public EmbeddedDatabase getDatabase() { if (this.dataSource == null) { initDatabase(); @@ -248,8 +245,7 @@ protected void shutdownDatabase() { * or if the database has been shut down. Subclasses may call this method to * access the {@code DataSource} instance directly. */ - @Nullable - protected final DataSource getDataSource() { + protected final @Nullable DataSource getDataSource() { return this.dataSource; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java index a1e050236775..a5659ac56461 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryBean.java @@ -18,12 +18,13 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.datasource.init.DatabasePopulator; import org.springframework.jdbc.datasource.init.DatabasePopulatorUtils; -import org.springframework.lang.Nullable; /** * A subclass of {@link EmbeddedDatabaseFactory} that implements {@link FactoryBean} @@ -44,8 +45,7 @@ public class EmbeddedDatabaseFactoryBean extends EmbeddedDatabaseFactory implements FactoryBean, InitializingBean, DisposableBean { - @Nullable - private DatabasePopulator databaseCleaner; + private @Nullable DatabasePopulator databaseCleaner; /** @@ -66,8 +66,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public DataSource getObject() { + public @Nullable DataSource getObject() { return getDataSource(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java index 9f44861ed8d0..dc4f5e1cf935 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHints.java @@ -18,10 +18,11 @@ import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; -import org.springframework.lang.Nullable; /** * {@link RuntimeHintsRegistrar} implementation that registers reflection hints diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java index c3a816e0cf15..14e92dba381c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/H2EmbeddedDatabaseConfigurer.java @@ -18,7 +18,8 @@ import java.sql.Driver; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** @@ -33,8 +34,7 @@ */ final class H2EmbeddedDatabaseConfigurer extends AbstractEmbeddedDatabaseConfigurer { - @Nullable - private static H2EmbeddedDatabaseConfigurer instance; + private static @Nullable H2EmbeddedDatabaseConfigurer instance; private final Class driverClass; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java index 8f3c69fe3bb2..e8c04da4c8ed 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/HsqlEmbeddedDatabaseConfigurer.java @@ -18,7 +18,8 @@ import java.sql.Driver; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ClassUtils; /** @@ -32,8 +33,7 @@ */ final class HsqlEmbeddedDatabaseConfigurer extends AbstractEmbeddedDatabaseConfigurer { - @Nullable - private static HsqlEmbeddedDatabaseConfigurer instance; + private static @Nullable HsqlEmbeddedDatabaseConfigurer instance; private final Class driverClass; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java index 2f9576f22e3f..2c181736a5e4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/embedded/package-info.java @@ -1,9 +1,7 @@ /** * Provides extensible support for creating embedded database instances. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.datasource.embedded; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java index 4a57474b5599..5e5cec82fc97 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/DataSourceInitializer.java @@ -18,9 +18,10 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -35,14 +36,11 @@ */ public class DataSourceInitializer implements InitializingBean, DisposableBean { - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; - @Nullable - private DatabasePopulator databasePopulator; + private @Nullable DatabasePopulator databasePopulator; - @Nullable - private DatabasePopulator databaseCleaner; + private @Nullable DatabasePopulator databaseCleaner; private boolean enabled = true; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java index 214a857620ba..45d768bb1ab4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulator.java @@ -23,9 +23,10 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -57,8 +58,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator { List scripts = new ArrayList<>(); - @Nullable - private String sqlScriptEncoding; + private @Nullable String sqlScriptEncoding; private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java index eb782d1e9835..09cd90d48d13 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptException.java @@ -16,8 +16,9 @@ package org.springframework.jdbc.datasource.init; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Root of the hierarchy of data access exceptions that are related to processing diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java index 7c9f3f06b17e..d414a82263bc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptParseException.java @@ -16,8 +16,9 @@ package org.springframework.jdbc.datasource.init; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; /** * Thrown by {@link ScriptUtils} if an SQL script cannot be properly parsed. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java index cf1d803b6101..0fbfd7b824a3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/ScriptUtils.java @@ -27,10 +27,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -330,7 +330,7 @@ static String readScript(EncodedResource resource, @Nullable String separator, } } - private static String readScript(LineNumberReader lineNumberReader, @Nullable String[] commentPrefixes, + private static String readScript(LineNumberReader lineNumberReader, String @Nullable [] commentPrefixes, @Nullable String separator, @Nullable String blockCommentEndDelimiter) throws IOException { String currentStatement = lineNumberReader.readLine(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java index 1e1d53e3a6c9..ac625201d2b4 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/init/package-info.java @@ -1,9 +1,7 @@ /** * Provides extensible support for initializing databases through scripts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.datasource.init; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java index b9d203f20234..02065d765d06 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/AbstractRoutingDataSource.java @@ -25,9 +25,10 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.datasource.AbstractDataSource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -44,21 +45,17 @@ */ public abstract class AbstractRoutingDataSource extends AbstractDataSource implements InitializingBean { - @Nullable - private Map targetDataSources; + private @Nullable Map targetDataSources; - @Nullable - private Object defaultTargetDataSource; + private @Nullable Object defaultTargetDataSource; private boolean lenientFallback = true; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); - @Nullable - private Map resolvedDataSources; + private @Nullable Map resolvedDataSources; - @Nullable - private DataSource resolvedDefaultDataSource; + private @Nullable DataSource resolvedDefaultDataSource; /** @@ -202,8 +199,7 @@ public Map getResolvedDataSources() { * @since 5.2.9 * @see #setDefaultTargetDataSource */ - @Nullable - public DataSource getResolvedDefaultDataSource() { + public @Nullable DataSource getResolvedDefaultDataSource() { return this.resolvedDefaultDataSource; } @@ -271,7 +267,6 @@ protected DataSource determineTargetDataSource() { * to match the stored lookup key type, as resolved by the * {@link #resolveSpecifiedLookupKey} method. */ - @Nullable - protected abstract Object determineCurrentLookupKey(); + protected abstract @Nullable Object determineCurrentLookupKey(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java index ff7e8825ae0e..a8eb53048b57 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/BeanFactoryDataSourceLookup.java @@ -18,10 +18,11 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +38,7 @@ */ public class BeanFactoryDataSourceLookup implements DataSourceLookup, BeanFactoryAware { - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java index 1ee290230ab7..70ca89daabbb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/IsolationLevelDataSourceRouter.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -133,8 +134,7 @@ else if (lookupKey instanceof String constantName) { } @Override - @Nullable - protected Object determineCurrentLookupKey() { + protected @Nullable Object determineCurrentLookupKey() { return TransactionSynchronizationManager.getCurrentTransactionIsolationLevel(); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java index 297a8051c17a..19a17d079f0b 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/MapDataSourceLookup.java @@ -22,7 +22,8 @@ import javax.sql.DataSource; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java index e5dd2ecb8616..901256a8a6ae 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/lookup/package-info.java @@ -1,9 +1,7 @@ /** * Provides a strategy for looking up JDBC DataSources by name. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.datasource.lookup; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java index baa79a5f8656..619f2e139d70 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/datasource/package-info.java @@ -3,9 +3,7 @@ * a PlatformTransactionManager for a single DataSource, * and various simple DataSource implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.datasource; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.java index c27089201d39..db4341ce152c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/GenericSqlQuery.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. @@ -18,9 +18,10 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.jdbc.core.RowMapper; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -36,12 +37,10 @@ */ public class GenericSqlQuery extends SqlQuery { - @Nullable - private RowMapper rowMapper; + private @Nullable RowMapper rowMapper; @SuppressWarnings("rawtypes") - @Nullable - private Class rowMapperClass; + private @Nullable Class rowMapperClass; /** @@ -71,7 +70,7 @@ public void afterPropertiesSet() { @Override @SuppressWarnings("unchecked") - protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context) { + protected RowMapper newRowMapper(@Nullable Object @Nullable [] parameters, @Nullable Map context) { if (this.rowMapper != null) { return this.rowMapper; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java index 86e38b0200e3..096896db00cc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQuery.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. @@ -22,7 +22,7 @@ import javax.sql.DataSource; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Reusable query in which concrete subclasses must implement the abstract @@ -63,8 +63,7 @@ public MappingSqlQuery(DataSource ds, String sql) { * @see #mapRow(ResultSet, int) */ @Override - @Nullable - protected final T mapRow(ResultSet rs, int rowNum, @Nullable Object[] parameters, @Nullable Map context) + protected final @Nullable T mapRow(ResultSet rs, int rowNum, @Nullable Object @Nullable [] parameters, @Nullable Map context) throws SQLException { return mapRow(rs, rowNum); @@ -83,7 +82,6 @@ protected final T mapRow(ResultSet rs, int rowNum, @Nullable Object[] parameters * Subclasses can simply not catch SQLExceptions, relying on the * framework to clean up. */ - @Nullable - protected abstract T mapRow(ResultSet rs, int rowNum) throws SQLException; + protected abstract @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java index 5743e3c76363..f631e1008d85 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/MappingSqlQueryWithParameters.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. @@ -22,8 +22,9 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.RowMapper; -import org.springframework.lang.Nullable; /** * Reusable RDBMS query in which concrete subclasses must implement @@ -73,7 +74,7 @@ public MappingSqlQueryWithParameters(DataSource ds, String sql) { * implementation of the mapRow() method. */ @Override - protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context) { + protected RowMapper newRowMapper(@Nullable Object @Nullable [] parameters, @Nullable Map context) { return new RowMapperImpl(parameters, context); } @@ -92,8 +93,7 @@ protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map * Subclasses can simply not catch SQLExceptions, relying on the * framework to clean up. */ - @Nullable - protected abstract T mapRow(ResultSet rs, int rowNum, @Nullable Object[] parameters, @Nullable Map context) + protected abstract @Nullable T mapRow(ResultSet rs, int rowNum, @Nullable Object @Nullable [] parameters, @Nullable Map context) throws SQLException; @@ -103,23 +103,20 @@ protected abstract T mapRow(ResultSet rs, int rowNum, @Nullable Object[] paramet */ protected class RowMapperImpl implements RowMapper { - @Nullable - private final Object[] params; + private final @Nullable Object @Nullable [] params; - @Nullable - private final Map context; + private final @Nullable Map context; /** * Use an array results. More efficient if we know how many results to expect. */ - public RowMapperImpl(@Nullable Object[] parameters, @Nullable Map context) { + public RowMapperImpl(@Nullable Object @Nullable [] parameters, @Nullable Map context) { this.params = parameters; this.context = context; } @Override - @Nullable - public T mapRow(ResultSet rs, int rowNum) throws SQLException { + public @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException { return MappingSqlQueryWithParameters.this.mapRow(rs, rowNum, this.params, this.context); } } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java index fe7b05b11131..19acf5eb7076 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/RdbmsOperation.java @@ -28,12 +28,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -73,11 +73,9 @@ public abstract class RdbmsOperation implements InitializingBean { private boolean returnGeneratedKeys = false; - @Nullable - private String[] generatedKeysColumnNames; + private String @Nullable [] generatedKeysColumnNames; - @Nullable - private String sql; + private @Nullable String sql; private final List declaredParameters = new ArrayList<>(); @@ -212,7 +210,7 @@ public boolean isReturnGeneratedKeys() { * Set the column names of the auto-generated keys. * @see java.sql.Connection#prepareStatement(String, String[]) */ - public void setGeneratedKeysColumnNames(@Nullable String... names) { + public void setGeneratedKeysColumnNames(String @Nullable ... names) { if (isCompiled()) { throw new InvalidDataAccessApiUsageException( "The column names for the generated keys must be set before the operation is compiled"); @@ -223,8 +221,7 @@ public void setGeneratedKeysColumnNames(@Nullable String... names) { /** * Return the column names of the auto generated keys. */ - @Nullable - public String[] getGeneratedKeysColumnNames() { + public String @Nullable [] getGeneratedKeysColumnNames() { return this.generatedKeysColumnNames; } @@ -239,8 +236,7 @@ public void setSql(@Nullable String sql) { * Subclasses can override this to supply dynamic SQL if they wish, but SQL is * normally set by calling the {@link #setSql} method or in a subclass constructor. */ - @Nullable - public String getSql() { + public @Nullable String getSql() { return this.sql; } @@ -264,7 +260,7 @@ protected String resolveSql() { * {@code java.sql.Types} class * @throws InvalidDataAccessApiUsageException if the operation is already compiled */ - public void setTypes(@Nullable int[] types) throws InvalidDataAccessApiUsageException { + public void setTypes(int @Nullable [] types) throws InvalidDataAccessApiUsageException { if (isCompiled()) { throw new InvalidDataAccessApiUsageException("Cannot add parameters once query is compiled"); } @@ -390,7 +386,7 @@ protected void checkCompiled() { * @param parameters the parameters supplied (may be {@code null}) * @throws InvalidDataAccessApiUsageException if the parameters are invalid */ - protected void validateParameters(@Nullable Object[] parameters) throws InvalidDataAccessApiUsageException { + protected void validateParameters(Object @Nullable [] parameters) throws InvalidDataAccessApiUsageException { checkCompiled(); int declaredInParameters = 0; for (SqlParameter param : this.declaredParameters) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java index 0f8600659f0e..5c3c182c5785 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlCall.java @@ -21,11 +21,12 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.CallableStatementCreator; import org.springframework.jdbc.core.CallableStatementCreatorFactory; import org.springframework.jdbc.core.ParameterMapper; import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -58,15 +59,13 @@ public abstract class SqlCall extends RdbmsOperation { * String of form {call add_invoice(?, ?, ?)} or {? = call get_invoice_count(?)} * if isFunction is set to true. Updated after each parameter is added. */ - @Nullable - private String callString; + private @Nullable String callString; /** * Object enabling us to create CallableStatementCreators * efficiently, based on this class's declared parameters. */ - @Nullable - private CallableStatementCreatorFactory callableStatementFactory; + private @Nullable CallableStatementCreatorFactory callableStatementFactory; /** @@ -177,8 +176,7 @@ protected void onCompileInternal() { /** * Get the call string. */ - @Nullable - public String getCallString() { + public @Nullable String getCallString() { return this.callString; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java index db33723bcd69..97bc6df719f5 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlFunction.java @@ -21,9 +21,10 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.TypeMismatchDataAccessException; import org.springframework.jdbc.core.SingleColumnRowMapper; -import org.springframework.lang.Nullable; /** * SQL "function" wrapper for a query that returns a single row of results. @@ -63,9 +64,7 @@ public class SqlFunction extends MappingSqlQuery { * @see #setSql * @see #compile */ - @SuppressWarnings("removal") public SqlFunction() { - setRowsExpected(1); } /** @@ -74,9 +73,7 @@ public SqlFunction() { * @param ds the DataSource to obtain connections from * @param sql the SQL to execute */ - @SuppressWarnings("removal") public SqlFunction(DataSource ds, String sql) { - setRowsExpected(1); setDataSource(ds); setSql(sql); } @@ -89,9 +86,7 @@ public SqlFunction(DataSource ds, String sql) { * {@code java.sql.Types} class * @see java.sql.Types */ - @SuppressWarnings("removal") public SqlFunction(DataSource ds, String sql, int[] types) { - setRowsExpected(1); setDataSource(ds); setSql(sql); setTypes(types); @@ -107,9 +102,7 @@ public SqlFunction(DataSource ds, String sql, int[] types) { * @see #setResultType(Class) * @see java.sql.Types */ - @SuppressWarnings("removal") public SqlFunction(DataSource ds, String sql, int[] types, Class resultType) { - setRowsExpected(1); setDataSource(ds); setSql(sql); setTypes(types); @@ -133,8 +126,7 @@ public void setResultType(Class resultType) { * of rows returned, this is treated as an error. */ @Override - @Nullable - protected T mapRow(ResultSet rs, int rowNum) throws SQLException { + protected @Nullable T mapRow(ResultSet rs, int rowNum) throws SQLException { return this.rowMapper.mapRow(rs, rowNum); } @@ -176,8 +168,7 @@ public int run(Object... parameters) { * returning the value as an object. * @return the value of the function */ - @Nullable - public Object runGeneric() { + public @Nullable Object runGeneric() { return findObject((Object[]) null, null); } @@ -186,8 +177,7 @@ public Object runGeneric() { * @param parameter single int parameter * @return the value of the function as an Object */ - @Nullable - public Object runGeneric(int parameter) { + public @Nullable Object runGeneric(int parameter) { return findObject(parameter); } @@ -199,8 +189,7 @@ public Object runGeneric(int parameter) { * @return the value of the function, as an Object * @see #execute(Object[]) */ - @Nullable - public Object runGeneric(Object[] parameters) { + public @Nullable Object runGeneric(Object[] parameters) { return findObject(parameters); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java index 94c4af9ebab0..0aa1efea0404 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlOperation.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,12 +16,13 @@ package org.springframework.jdbc.object; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.PreparedStatementCreatorFactory; import org.springframework.jdbc.core.PreparedStatementSetter; import org.springframework.jdbc.core.namedparam.NamedParameterUtils; import org.springframework.jdbc.core.namedparam.ParsedSql; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -40,12 +41,10 @@ public abstract class SqlOperation extends RdbmsOperation { * Object enabling us to create PreparedStatementCreators efficiently, * based on this class's declared parameters. */ - @Nullable - private PreparedStatementCreatorFactory preparedStatementFactory; + private @Nullable PreparedStatementCreatorFactory preparedStatementFactory; /** Parsed representation of the SQL statement. */ - @Nullable - private ParsedSql cachedSql; + private @Nullable ParsedSql cachedSql; /** Monitor for locking the cached representation of the parsed SQL statement. */ private final Object parsedSqlMonitor = new Object(); @@ -95,7 +94,7 @@ protected ParsedSql getParsedSql() { * with the given parameters. * @param params the parameter array (may be {@code null}) */ - protected final PreparedStatementSetter newPreparedStatementSetter(@Nullable Object[] params) { + protected final PreparedStatementSetter newPreparedStatementSetter(@Nullable Object @Nullable [] params) { Assert.state(this.preparedStatementFactory != null, "No PreparedStatementFactory available"); return this.preparedStatementFactory.newPreparedStatementSetter(params); } @@ -105,7 +104,7 @@ protected final PreparedStatementSetter newPreparedStatementSetter(@Nullable Obj * with the given parameters. * @param params the parameter array (may be {@code null}) */ - protected final PreparedStatementCreator newPreparedStatementCreator(@Nullable Object[] params) { + protected final PreparedStatementCreator newPreparedStatementCreator(@Nullable Object @Nullable [] params) { Assert.state(this.preparedStatementFactory != null, "No PreparedStatementFactory available"); return this.preparedStatementFactory.newPreparedStatementCreator(params); } @@ -117,7 +116,7 @@ protected final PreparedStatementCreator newPreparedStatementCreator(@Nullable O * the factory's, for example because of named parameter expanding) * @param params the parameter array (may be {@code null}) */ - protected final PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, @Nullable Object[] params) { + protected final PreparedStatementCreator newPreparedStatementCreator(String sqlToUse, @Nullable Object @Nullable [] params) { Assert.state(this.preparedStatementFactory != null, "No PreparedStatementFactory available"); return this.preparedStatementFactory.newPreparedStatementCreator(sqlToUse, params); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java index 994329c32025..fd7d50e37997 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlQuery.java @@ -18,16 +18,20 @@ import java.util.List; import java.util.Map; +import java.util.function.BiFunction; +import java.util.stream.Stream; import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.support.DataAccessUtils; +import org.springframework.jdbc.core.PreparedStatementCreator; import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import org.springframework.jdbc.core.namedparam.NamedParameterUtils; import org.springframework.jdbc.core.namedparam.ParsedSql; -import org.springframework.lang.Nullable; /** * Reusable operation object representing an SQL query. @@ -51,15 +55,12 @@ * @author Rod Johnson * @author Juergen Hoeller * @author Thomas Risberg + * @author Yanming Zhou * @param the result type * @see SqlUpdate */ public abstract class SqlQuery extends SqlOperation { - /** The number of rows to expect; if 0, unknown. */ - private int rowsExpected = 0; - - /** * Constructor to allow use as a JavaBean. *

    The {@code DataSource} and SQL must be supplied before @@ -81,43 +82,38 @@ public SqlQuery(DataSource ds, String sql) { /** - * Set the number of rows expected. - *

    This can be used to ensure efficient storage of results. The - * default behavior is not to expect any specific number of rows. - * @deprecated since 6.2.4 with no replacement since the property has never - * had any affect on behavior; to be removed in 7.0 - */ - @Deprecated(since = "6.2.4", forRemoval = true) - public void setRowsExpected(int rowsExpected) { - this.rowsExpected = rowsExpected; - } - - /** - * Get the number of rows expected. - * @deprecated since 6.2.4 with no replacement since the property has never - * had any affect on behavior; to be removed in 7.0 + * Central execution method. All un-named parameter execution goes through this method. + * @param params parameters, similar to JDO query parameters. + * Primitive parameters must be represented by their Object wrapper type. + * The ordering of parameters is significant. + * @param context the contextual information passed to the {@code mapRow} + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a List of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. */ - @Deprecated(since = "6.2.4", forRemoval = true) - public int getRowsExpected() { - return this.rowsExpected; + public List execute(Object @Nullable [] params, @Nullable Map context) throws DataAccessException { + validateParameters(params); + RowMapper rowMapper = newRowMapper(params, context); + return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper); } - /** - * Central execution method. All un-named parameter execution goes through this method. + * Central stream method. All un-named parameter execution goes through this method. * @param params parameters, similar to JDO query parameters. * Primitive parameters must be represented by their Object wrapper type. * The ordering of parameters is significant. * @param context the contextual information passed to the {@code mapRow} * callback method. The JDBC operation itself doesn't rely on this parameter, * but it can be useful for creating the objects of the result list. - * @return a List of objects, one per row of the ResultSet. Normally all these + * @return a result Stream of objects, one per row of the ResultSet. Normally all these * will be of the same class, although it is possible to use different types. + * @since 7.0 */ - public List execute(@Nullable Object[] params, @Nullable Map context) throws DataAccessException { + public Stream stream(Object @Nullable [] params, @Nullable Map context) throws DataAccessException { validateParameters(params); RowMapper rowMapper = newRowMapper(params, context); - return getJdbcTemplate().query(newPreparedStatementCreator(params), rowMapper); + return getJdbcTemplate().queryForStream(newPreparedStatementCreator(params), rowMapper); } /** @@ -130,6 +126,17 @@ public List execute(Object... params) throws DataAccessException { return execute(params, null); } + /** + * Convenient method to stream without context. + * @param params parameters for the query. Primitive parameters must + * be represented by their Object wrapper type. The ordering of parameters is + * significant. + * @since 7.0 + */ + public Stream stream(Object... params) throws DataAccessException { + return stream(params, null); + } + /** * Convenient method to execute without parameters. * @param context the contextual information for object creation @@ -138,6 +145,15 @@ public List execute(Map context) throws DataAccessException { return execute((Object[]) null, context); } + /** + * Convenient method to stream without parameters. + * @param context the contextual information for object creation + * @since 7.0 + */ + public Stream stream(Map context) throws DataAccessException { + return stream(null, context); + } + /** * Convenient method to execute without parameters nor context. */ @@ -145,6 +161,14 @@ public List execute() throws DataAccessException { return execute((Object[]) null, null); } + /** + * Convenient method to stream without parameters nor context. + * @since 7.0 + */ + public Stream stream() throws DataAccessException { + return stream(null, null); + } + /** * Convenient method to execute with a single int parameter and context. * @param p1 single int parameter @@ -228,13 +252,24 @@ public List execute(String p1) throws DataAccessException { * will be of the same class, although it is possible to use different types. */ public List executeByNamedParam(Map paramMap, @Nullable Map context) throws DataAccessException { - validateNamedParameters(paramMap); - ParsedSql parsedSql = getParsedSql(); - MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); - String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); - Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); - RowMapper rowMapper = newRowMapper(params, context); - return getJdbcTemplate().query(newPreparedStatementCreator(sqlToUse, params), rowMapper); + return queryByNamedParam(paramMap, context, getJdbcTemplate()::query); + } + + /** + * Central stream method. All named parameter execution goes through this method. + * @param paramMap parameters associated with the name specified while declaring + * the SqlParameters. Primitive parameters must be represented by their Object wrapper + * type. The ordering of parameters is not significant since they are supplied in a + * SqlParameterMap which is an implementation of the Map interface. + * @param context the contextual information passed to the {@code mapRow} + * callback method. The JDBC operation itself doesn't rely on this parameter, + * but it can be useful for creating the objects of the result list. + * @return a Stream of objects, one per row of the ResultSet. Normally all these + * will be of the same class, although it is possible to use different types. + * @since 7.0 + */ + public Stream streamByNamedParam(Map paramMap, @Nullable Map context) throws DataAccessException { + return queryByNamedParam(paramMap, context, getJdbcTemplate()::queryForStream); } /** @@ -243,10 +278,31 @@ public List executeByNamedParam(Map paramMap, @Nullable Map * the SqlParameters. Primitive parameters must be represented by their Object wrapper * type. The ordering of parameters is not significant. */ - public List executeByNamedParam(Map paramMap) throws DataAccessException { + public List executeByNamedParam(Map paramMap) throws DataAccessException { return executeByNamedParam(paramMap, null); } + /** + * Convenient method to stream without context. + * @param paramMap parameters associated with the name specified while declaring + * the SqlParameters. Primitive parameters must be represented by their Object wrapper + * type. The ordering of parameters is not significant. + * @since 7.0 + */ + public Stream streamByNamedParam(Map paramMap) throws DataAccessException { + return streamByNamedParam(paramMap, null); + } + + private R queryByNamedParam(Map paramMap, @Nullable Map context, BiFunction, R> queryFunction) { + validateNamedParameters(paramMap); + ParsedSql parsedSql = getParsedSql(); + MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); + String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); + @Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + RowMapper rowMapper = newRowMapper(params, context); + return queryFunction.apply(newPreparedStatementCreator(sqlToUse, params), rowMapper); + } + /** * Generic object finder method, used by all other {@code findObject} methods. @@ -256,8 +312,7 @@ public List executeByNamedParam(Map paramMap) throws DataAccessExc * choose to treat this as an error and throw an exception. * @see org.springframework.dao.support.DataAccessUtils#singleResult */ - @Nullable - public T findObject(@Nullable Object[] params, @Nullable Map context) throws DataAccessException { + public @Nullable T findObject(Object @Nullable [] params, @Nullable Map context) throws DataAccessException { List results = execute(params, context); return DataAccessUtils.singleResult(results); } @@ -265,8 +320,7 @@ public T findObject(@Nullable Object[] params, @Nullable Map context) thro /** * Convenient method to find a single object without context. */ - @Nullable - public T findObject(Object... params) throws DataAccessException { + public @Nullable T findObject(Object... params) throws DataAccessException { return findObject(params, null); } @@ -274,16 +328,14 @@ public T findObject(Object... params) throws DataAccessException { * Convenient method to find a single object given a single int parameter * and a context. */ - @Nullable - public T findObject(int p1, @Nullable Map context) throws DataAccessException { + public @Nullable T findObject(int p1, @Nullable Map context) throws DataAccessException { return findObject(new Object[] {p1}, context); } /** * Convenient method to find a single object given a single int parameter. */ - @Nullable - public T findObject(int p1) throws DataAccessException { + public @Nullable T findObject(int p1) throws DataAccessException { return findObject(p1, null); } @@ -291,16 +343,14 @@ public T findObject(int p1) throws DataAccessException { * Convenient method to find a single object given two int parameters * and a context. */ - @Nullable - public T findObject(int p1, int p2, @Nullable Map context) throws DataAccessException { + public @Nullable T findObject(int p1, int p2, @Nullable Map context) throws DataAccessException { return findObject(new Object[] {p1, p2}, context); } /** * Convenient method to find a single object given two int parameters. */ - @Nullable - public T findObject(int p1, int p2) throws DataAccessException { + public @Nullable T findObject(int p1, int p2) throws DataAccessException { return findObject(p1, p2, null); } @@ -308,16 +358,14 @@ public T findObject(int p1, int p2) throws DataAccessException { * Convenient method to find a single object given a single long parameter * and a context. */ - @Nullable - public T findObject(long p1, @Nullable Map context) throws DataAccessException { + public @Nullable T findObject(long p1, @Nullable Map context) throws DataAccessException { return findObject(new Object[] {p1}, context); } /** * Convenient method to find a single object given a single long parameter. */ - @Nullable - public T findObject(long p1) throws DataAccessException { + public @Nullable T findObject(long p1) throws DataAccessException { return findObject(p1, null); } @@ -325,16 +373,14 @@ public T findObject(long p1) throws DataAccessException { * Convenient method to find a single object given a single String parameter * and a context. */ - @Nullable - public T findObject(String p1, @Nullable Map context) throws DataAccessException { + public @Nullable T findObject(String p1, @Nullable Map context) throws DataAccessException { return findObject(new Object[] {p1}, context); } /** * Convenient method to find a single object given a single String parameter. */ - @Nullable - public T findObject(String p1) throws DataAccessException { + public @Nullable T findObject(String p1) throws DataAccessException { return findObject(p1, null); } @@ -349,8 +395,7 @@ public T findObject(String p1) throws DataAccessException { * @return a List of objects, one per row of the ResultSet. Normally all these * will be of the same class, although it is possible to use different types. */ - @Nullable - public T findObjectByNamedParam(Map paramMap, @Nullable Map context) throws DataAccessException { + public @Nullable T findObjectByNamedParam(Map paramMap, @Nullable Map context) throws DataAccessException { List results = executeByNamedParam(paramMap, context); return DataAccessUtils.singleResult(results); } @@ -361,8 +406,7 @@ public T findObjectByNamedParam(Map paramMap, @Nullable Map con * matching named parameters specified in the SQL statement. * Ordering is not significant. */ - @Nullable - public T findObjectByNamedParam(Map paramMap) throws DataAccessException { + public @Nullable T findObjectByNamedParam(Map paramMap) throws DataAccessException { return findObjectByNamedParam(paramMap, null); } @@ -378,6 +422,6 @@ public T findObjectByNamedParam(Map paramMap) throws DataAccessExcept * but it can be useful for creating the objects of the result list. * @see #execute */ - protected abstract RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context); + protected abstract RowMapper newRowMapper(@Nullable Object @Nullable [] parameters, @Nullable Map context); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java index a68e87c14cd9..f85358ea808e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/SqlUpdate.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. @@ -20,6 +20,8 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException; @@ -252,7 +254,7 @@ public int updateByNamedParam(Map paramMap) throws DataAccessExceptio ParsedSql parsedSql = getParsedSql(); MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); - Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + @Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(sqlToUse, params)); checkRowsAffected(rowsAffected); return rowsAffected; @@ -271,7 +273,7 @@ public int updateByNamedParam(Map paramMap, KeyHolder generatedKeyHol ParsedSql parsedSql = getParsedSql(); MapSqlParameterSource paramSource = new MapSqlParameterSource(paramMap); String sqlToUse = NamedParameterUtils.substituteNamedParameters(parsedSql, paramSource); - Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); + @Nullable Object[] params = NamedParameterUtils.buildValueArray(parsedSql, paramSource, getDeclaredParameters()); int rowsAffected = getJdbcTemplate().update(newPreparedStatementCreator(sqlToUse, params), generatedKeyHolder); checkRowsAffected(rowsAffected); return rowsAffected; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java index 1c3fa977d2b4..d2fef590b628 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/UpdatableSqlQuery.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. @@ -22,8 +22,9 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.core.RowMapper; -import org.springframework.lang.Nullable; /** * Reusable RDBMS query in which concrete subclasses must implement @@ -62,7 +63,7 @@ public UpdatableSqlQuery(DataSource ds, String sql) { * implementation of the {@code updateRow()} method. */ @Override - protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map context) { + protected RowMapper newRowMapper(@Nullable Object @Nullable [] parameters, @Nullable Map context) { return new RowMapperImpl(context); } @@ -90,8 +91,7 @@ protected RowMapper newRowMapper(@Nullable Object[] parameters, @Nullable Map */ protected class RowMapperImpl implements RowMapper { - @Nullable - private final Map context; + private final @Nullable Map context; public RowMapperImpl(@Nullable Map context) { this.context = context; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java index 727aa94964f0..12744377641c 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/object/package-info.java @@ -14,9 +14,7 @@ * Expert One-On-One J2EE Design and Development * by Rod Johnson (Wrox, 2002). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.object; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java index 86f4c59af6af..b2a919f527f6 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/package-info.java @@ -17,9 +17,7 @@ * Expert One-On-One J2EE Design and Development * by Rod Johnson (Wrox, 2002). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java index f82e8475ce1d..89ac060fcf93 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/AbstractFallbackSQLExceptionTranslator.java @@ -20,9 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -41,11 +41,9 @@ public abstract class AbstractFallbackSQLExceptionTranslator implements SQLExcep /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private SQLExceptionTranslator fallbackTranslator; + private @Nullable SQLExceptionTranslator fallbackTranslator; - @Nullable - private SQLExceptionTranslator customTranslator; + private @Nullable SQLExceptionTranslator customTranslator; /** @@ -60,8 +58,7 @@ public void setFallbackTranslator(@Nullable SQLExceptionTranslator fallback) { * Return the fallback exception translator, if any. * @see #setFallbackTranslator */ - @Nullable - public SQLExceptionTranslator getFallbackTranslator() { + public @Nullable SQLExceptionTranslator getFallbackTranslator() { return this.fallbackTranslator; } @@ -80,8 +77,7 @@ public void setCustomTranslator(@Nullable SQLExceptionTranslator customTranslato * @since 6.1 * @see #setCustomTranslator */ - @Nullable - public SQLExceptionTranslator getCustomTranslator() { + public @Nullable SQLExceptionTranslator getCustomTranslator() { return this.customTranslator; } @@ -91,8 +87,7 @@ public SQLExceptionTranslator getCustomTranslator() { * {@link #getFallbackTranslator() fallback translator} if necessary. */ @Override - @Nullable - public DataAccessException translate(String task, @Nullable String sql, SQLException ex) { + public @Nullable DataAccessException translate(String task, @Nullable String sql, SQLException ex) { Assert.notNull(ex, "Cannot translate a null SQLException"); SQLExceptionTranslator custom = getCustomTranslator(); @@ -130,8 +125,7 @@ public DataAccessException translate(String task, @Nullable String sql, SQLExcep * @return the DataAccessException, wrapping the {@code SQLException}; * or {@code null} if no exception match found */ - @Nullable - protected abstract DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex); + protected abstract @Nullable DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex); /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java index 8c84cb34074d..d27d4793102a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLErrorCodesTranslation.java @@ -16,8 +16,9 @@ package org.springframework.jdbc.support; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -33,8 +34,7 @@ public class CustomSQLErrorCodesTranslation { private String[] errorCodes = new String[0]; - @Nullable - private Class exceptionClass; + private @Nullable Class exceptionClass; /** @@ -65,8 +65,7 @@ public void setExceptionClass(@Nullable Class exceptionClass) { /** * Return the exception class for the specified error codes. */ - @Nullable - public Class getExceptionClass() { + public @Nullable Class getExceptionClass() { return this.exceptionClass; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java index 259353a04f73..ce8e6d2844cd 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/CustomSQLExceptionTranslatorRegistry.java @@ -21,8 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Registry for custom {@link SQLExceptionTranslator} instances associated with @@ -91,8 +90,7 @@ public void registerTranslator(String dbName, SQLExceptionTranslator translator) * @param dbName the database name * @return the custom translator, or {@code null} if none found */ - @Nullable - public SQLExceptionTranslator findTranslatorForDatabase(String dbName) { + public @Nullable SQLExceptionTranslator findTranslatorForDatabase(String dbName) { return this.translatorMap.get(dbName); } 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 f518103d2e3b..0654bcb8038e 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. @@ -25,10 +25,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.jdbc.CannotGetJdbcConnectionException; -import org.springframework.lang.Nullable; /** * Bean that checks if a database has already started up. To be referenced @@ -57,11 +57,9 @@ public class DatabaseStartupValidator implements InitializingBean { protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; - @Nullable - private String validationQuery; + private @Nullable String validationQuery; private int interval = DEFAULT_INTERVAL; @@ -77,9 +75,9 @@ public void setDataSource(DataSource dataSource) { /** * 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/GeneratedKeyHolder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java index d1c26195edb8..4bca6a858d2e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/GeneratedKeyHolder.java @@ -21,9 +21,10 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataRetrievalFailureException; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; /** * The standard implementation of the {@link KeyHolder} interface, to be used for @@ -60,14 +61,12 @@ public GeneratedKeyHolder(List> keyList) { @Override - @Nullable - public Number getKey() throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { + public @Nullable Number getKey() throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { return getKeyAs(Number.class); } @Override - @Nullable - public T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { + public @Nullable T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageException, DataRetrievalFailureException { if (this.keyList.isEmpty()) { return null; } @@ -94,8 +93,7 @@ public T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageExceptio } @Override - @Nullable - public Map getKeys() throws InvalidDataAccessApiUsageException { + public @Nullable Map getKeys() throws InvalidDataAccessApiUsageException { if (this.keyList.isEmpty()) { return null; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java index 5178b4a0f4e6..7b778b73d72d 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcAccessor.java @@ -20,9 +20,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -43,11 +43,9 @@ public abstract class JdbcAccessor implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; - @Nullable - private volatile SQLExceptionTranslator exceptionTranslator; + private volatile @Nullable SQLExceptionTranslator exceptionTranslator; private boolean lazyInit = true; @@ -62,8 +60,7 @@ public void setDataSource(@Nullable DataSource dataSource) { /** * Return the DataSource used by this template. */ - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { return this.dataSource; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java index 6d630cc5f530..82770bde48a3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/JdbcTransactionManager.java @@ -20,9 +20,10 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.lang.Nullable; /** * {@link JdbcAccessor}-aligned subclass of the plain {@link DataSourceTransactionManager}, @@ -52,8 +53,7 @@ @SuppressWarnings("serial") public class JdbcTransactionManager extends DataSourceTransactionManager { - @Nullable - private volatile SQLExceptionTranslator exceptionTranslator; + private volatile @Nullable SQLExceptionTranslator exceptionTranslator; private boolean lazyInit = true; 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 84867940ef6c..3054008313e9 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. @@ -36,10 +36,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jdbc.CannotGetJdbcConnectionException; import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.lang.Nullable; import org.springframework.util.NumberUtils; import org.springframework.util.StringUtils; @@ -150,8 +150,7 @@ public static void closeResultSet(@Nullable ResultSet rs) { * @throws SQLException if thrown by the JDBC API * @see #getResultSetValue(ResultSet, int) */ - @Nullable - public static Object getResultSetValue(ResultSet rs, int index, @Nullable Class requiredType) throws SQLException { + public static @Nullable Object getResultSetValue(ResultSet rs, int index, @Nullable Class requiredType) throws SQLException { if (requiredType == null) { return getResultSetValue(rs, index); } @@ -274,8 +273,7 @@ else if (obj instanceof Number number) { * @see java.sql.Clob * @see java.sql.Timestamp */ - @Nullable - public static Object getResultSetValue(ResultSet rs, int index) throws SQLException { + public static @Nullable Object getResultSetValue(ResultSet rs, int index) throws SQLException { Object obj = rs.getObject(index); String className = null; if (obj != null) { @@ -377,11 +375,11 @@ public static T extractDatabaseMetaData(DataSource dataSource, DatabaseMetaD * @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 { @@ -445,8 +443,7 @@ public static boolean supportsBatchUpdates(Connection con) { * @param source the name as provided in database meta-data * @return the common name to be used (for example, "DB2" or "Sybase") */ - @Nullable - public static String commonDatabaseName(@Nullable String source) { + public static @Nullable String commonDatabaseName(@Nullable String source) { String name = source; if (source != null && source.startsWith("DB2")) { name = "DB2"; @@ -479,8 +476,7 @@ public static boolean isNumeric(int sqlType) { * (for example, "VARCHAR"/"NUMERIC"), or {@code null} if not resolvable * @since 5.2 */ - @Nullable - public static String resolveTypeName(int sqlType) { + public static @Nullable String resolveTypeName(int sqlType) { return typeNames.get(sqlType); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java index e022341d2720..15274ef2112a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/KeyHolder.java @@ -19,8 +19,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; /** * Interface for retrieving keys, typically used for auto-generated keys @@ -58,8 +59,7 @@ public interface KeyHolder { * @throws InvalidDataAccessApiUsageException if multiple keys are encountered * @see #getKeyAs(Class) */ - @Nullable - Number getKey() throws InvalidDataAccessApiUsageException; + @Nullable Number getKey() throws InvalidDataAccessApiUsageException; /** * Retrieve the first item from the first map, assuming that there is just @@ -76,8 +76,7 @@ public interface KeyHolder { * @since 5.3 * @see #getKey() */ - @Nullable - T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageException; + @Nullable T getKeyAs(Class keyType) throws InvalidDataAccessApiUsageException; /** * Retrieve the first map of keys. @@ -86,8 +85,7 @@ public interface KeyHolder { * @return the Map of generated keys for a single row * @throws InvalidDataAccessApiUsageException if keys for multiple rows are encountered */ - @Nullable - Map getKeys() throws InvalidDataAccessApiUsageException; + @Nullable Map getKeys() throws InvalidDataAccessApiUsageException; /** * Return a reference to the List that contains the keys. diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java index b7e0af91d57c..f402e9e1b239 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslator.java @@ -23,6 +23,8 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.ClassPathResource; import org.springframework.dao.CannotAcquireLockException; import org.springframework.dao.DataAccessException; @@ -33,7 +35,6 @@ import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.InvalidResultSetAccessException; -import org.springframework.lang.Nullable; import org.springframework.util.function.SingletonSupplier; import org.springframework.util.function.SupplierUtils; @@ -84,8 +85,7 @@ public class SQLErrorCodeSQLExceptionTranslator extends AbstractFallbackSQLExcep new ClassPathResource(SQLErrorCodesFactory.SQL_ERROR_CODE_OVERRIDE_PATH, SQLErrorCodesFactory.class.getClassLoader()).exists(); - @Nullable - private SingletonSupplier sqlErrorCodes; + private @Nullable SingletonSupplier sqlErrorCodes; /** @@ -173,16 +173,14 @@ public void setSqlErrorCodes(@Nullable SQLErrorCodes sec) { * Usually determined via a DataSource. * @see #setDataSource */ - @Nullable - public SQLErrorCodes getSqlErrorCodes() { + public @Nullable SQLErrorCodes getSqlErrorCodes() { return SupplierUtils.resolve(this.sqlErrorCodes); } @SuppressWarnings("deprecation") @Override - @Nullable - protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { + protected @Nullable DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { SQLException sqlEx = ex; if (sqlEx instanceof BatchUpdateException && sqlEx.getNextException() != null) { SQLException nestedSqlEx = sqlEx.getNextException(); @@ -312,8 +310,7 @@ else if (Arrays.binarySearch(sqlErrorCodes.getCannotSerializeTransactionCodes(), * @deprecated as of 6.1, in favor of {@link #setCustomTranslator} */ @Deprecated(since = "6.1") - @Nullable - protected DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlEx) { + protected @Nullable DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlEx) { return null; } @@ -330,8 +327,7 @@ protected DataAccessException customTranslate(String task, @Nullable String sql, * {@code sqlEx} parameter as a nested root cause. * @see CustomSQLErrorCodesTranslation#setExceptionClass */ - @Nullable - protected DataAccessException createCustomException( + protected @Nullable DataAccessException createCustomException( String task, @Nullable String sql, SQLException sqlEx, Class exceptionClass) { // Find appropriate constructor for the given exception class diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java index 5d4498c05230..a990e33401b6 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodes.java @@ -16,7 +16,8 @@ package org.springframework.jdbc.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -35,8 +36,7 @@ */ public class SQLErrorCodes { - @Nullable - private String[] databaseProductNames; + private String @Nullable [] databaseProductNames; private boolean useSqlStateForTranslation = false; @@ -60,11 +60,9 @@ public class SQLErrorCodes { private String[] cannotSerializeTransactionCodes = new String[0]; - @Nullable - private CustomSQLErrorCodesTranslation[] customTranslations; + private CustomSQLErrorCodesTranslation @Nullable [] customTranslations; - @Nullable - private SQLExceptionTranslator customSqlExceptionTranslator; + private @Nullable SQLExceptionTranslator customSqlExceptionTranslator; /** @@ -75,8 +73,7 @@ public void setDatabaseProductName(@Nullable String databaseProductName) { this.databaseProductNames = new String[] {databaseProductName}; } - @Nullable - public String getDatabaseProductName() { + public @Nullable String getDatabaseProductName() { return (this.databaseProductNames != null && this.databaseProductNames.length > 0 ? this.databaseProductNames[0] : null); } @@ -85,12 +82,11 @@ public String getDatabaseProductName() { * Set this property to specify multiple database names that contains spaces, * in which case we can not use bean names for lookup. */ - public void setDatabaseProductNames(@Nullable String... databaseProductNames) { + public void setDatabaseProductNames(String @Nullable ... databaseProductNames) { this.databaseProductNames = databaseProductNames; } - @Nullable - public String[] getDatabaseProductNames() { + public String @Nullable [] getDatabaseProductNames() { return this.databaseProductNames; } @@ -190,8 +186,7 @@ public void setCustomTranslations(CustomSQLErrorCodesTranslation... customTransl this.customTranslations = customTranslations; } - @Nullable - public CustomSQLErrorCodesTranslation[] getCustomTranslations() { + public CustomSQLErrorCodesTranslation @Nullable [] getCustomTranslations() { return this.customTranslations; } @@ -214,8 +209,7 @@ public void setCustomSqlExceptionTranslator(@Nullable SQLExceptionTranslator cus this.customSqlExceptionTranslator = customSqlExceptionTranslator; } - @Nullable - public SQLExceptionTranslator getCustomSqlExceptionTranslator() { + public @Nullable SQLExceptionTranslator getCustomSqlExceptionTranslator() { return this.customSqlExceptionTranslator; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java index 4b4575cc8e96..65461393da08 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLErrorCodesFactory.java @@ -24,13 +24,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.xml.XmlBeanDefinitionReader; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.PatternMatchUtils; @@ -71,8 +71,7 @@ public class SQLErrorCodesFactory { * Lazily initialized in order to avoid making {@code SQLErrorCodesFactory} constructor * reachable on native images when not needed. */ - @Nullable - private static SQLErrorCodesFactory instance; + private static @Nullable SQLErrorCodesFactory instance; /** @@ -156,8 +155,7 @@ protected SQLErrorCodesFactory() { * @return the resource, or {@code null} if the resource wasn't found * @see #getInstance */ - @Nullable - protected Resource loadResource(String path) { + protected @Nullable Resource loadResource(String path) { return new ClassPathResource(path, getClass().getClassLoader()); } @@ -223,8 +221,7 @@ public SQLErrorCodes getErrorCodes(DataSource dataSource) { * @since 5.2.9 * @see java.sql.DatabaseMetaData#getDatabaseProductName() */ - @Nullable - public SQLErrorCodes resolveErrorCodes(DataSource dataSource) { + public @Nullable SQLErrorCodes resolveErrorCodes(DataSource dataSource) { Assert.notNull(dataSource, "DataSource must not be null"); if (logger.isDebugEnabled()) { logger.debug("Looking up default SQLErrorCodes for DataSource [" + identify(dataSource) + "]"); @@ -286,8 +283,7 @@ public SQLErrorCodes registerDatabase(DataSource dataSource, String databaseName * @since 4.3.5 * @see #registerDatabase(DataSource, String) */ - @Nullable - public SQLErrorCodes unregisterDatabase(DataSource dataSource) { + public @Nullable SQLErrorCodes unregisterDatabase(DataSource dataSource) { return this.dataSourceCache.remove(dataSource); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java index eb5fdcd25024..214c1d92ce26 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionSubclassTranslator.java @@ -30,6 +30,8 @@ import java.sql.SQLTransientConnectionException; import java.sql.SQLTransientException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.CannotAcquireLockException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; @@ -42,7 +44,6 @@ import org.springframework.dao.RecoverableDataAccessException; import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.jdbc.BadSqlGrammarException; -import org.springframework.lang.Nullable; /** * {@link SQLExceptionTranslator} implementation which analyzes the specific @@ -67,8 +68,7 @@ public SQLExceptionSubclassTranslator() { } @Override - @Nullable - protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { + protected @Nullable DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { if (ex instanceof SQLTransientException) { if (ex instanceof SQLTransientConnectionException) { return new TransientDataAccessResourceException(buildMessage(task, sql, ex), ex); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java index 349ea1d29c70..4c52546c9c08 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLExceptionTranslator.java @@ -18,8 +18,9 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Strategy interface for translating between {@link SQLException SQLExceptions} @@ -52,7 +53,6 @@ public interface SQLExceptionTranslator { * or {@code null} if no specific translation could be applied * @see org.springframework.dao.DataAccessException#getRootCause() */ - @Nullable - DataAccessException translate(String task, @Nullable String sql, SQLException ex); + @Nullable DataAccessException translate(String task, @Nullable String sql, SQLException ex); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java index ad68fcf54d40..2e9e04b1d2d1 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslator.java @@ -19,6 +19,8 @@ import java.sql.SQLException; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.CannotAcquireLockException; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; @@ -28,7 +30,6 @@ import org.springframework.dao.QueryTimeoutException; import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.jdbc.BadSqlGrammarException; -import org.springframework.lang.Nullable; /** * {@link SQLExceptionTranslator} implementation that analyzes the SQL state in @@ -99,8 +100,7 @@ public class SQLStateSQLExceptionTranslator extends AbstractFallbackSQLException @Override - @Nullable - protected DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { + protected @Nullable DataAccessException doTranslate(String task, @Nullable String sql, SQLException ex) { // First, the getSQLState check... String sqlState = getSqlState(ex); if (sqlState != null && sqlState.length() >= 2) { @@ -149,8 +149,7 @@ else if (PESSIMISTIC_LOCKING_FAILURE_CODES.contains(classCode)) { * is to be extracted * @return the SQL state code */ - @Nullable - private String getSqlState(SQLException ex) { + private @Nullable String getSqlState(SQLException ex) { String sqlState = ex.getSQLState(); if (sqlState == null) { SQLException nestedEx = ex.getNextException(); diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlArrayValue.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlArrayValue.java index a6834efaed89..4f4fc1139698 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlArrayValue.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/SqlArrayValue.java @@ -20,8 +20,9 @@ import java.sql.PreparedStatement; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -41,8 +42,7 @@ public class SqlArrayValue implements SqlValue { private final Object[] elements; - @Nullable - private Array array; + private @Nullable Array array; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java index e1cc97c11172..8e3483f6db55 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractColumnMaxValueIncrementer.java @@ -43,7 +43,7 @@ public abstract class AbstractColumnMaxValueIncrementer extends AbstractDataFiel * @see #setIncrementerName * @see #setColumnName */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway.Init") public AbstractColumnMaxValueIncrementer() { } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java index 6f761ea9861e..3b37b64cc14f 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractDataFieldMaxValueIncrementer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,7 +20,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -35,11 +34,11 @@ */ public abstract class AbstractDataFieldMaxValueIncrementer implements DataFieldMaxValueIncrementer, InitializingBean { - @Nullable + @SuppressWarnings("NullAway.Init") private DataSource dataSource; /** The name of the sequence/table containing the sequence. */ - @Nullable + @SuppressWarnings("NullAway.Init") private String incrementerName; /** The length to which a string result should be pre-pended with zeroes. */ @@ -77,7 +76,6 @@ public void setDataSource(DataSource dataSource) { /** * Return the data source to retrieve the value from. */ - @SuppressWarnings("NullAway") public DataSource getDataSource() { return this.dataSource; } @@ -92,7 +90,6 @@ public void setIncrementerName(String incrementerName) { /** * Return the name of the sequence/table. */ - @SuppressWarnings("NullAway") public String getIncrementerName() { return this.incrementerName; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java index 39b400ad8434..dcaaeb0eca38 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/AbstractIdentityColumnMaxValueIncrementer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2021 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,10 +23,13 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.support.JdbcUtils; +import org.springframework.util.Assert; /** * Abstract base class for {@link DataFieldMaxValueIncrementer} implementations @@ -41,7 +44,7 @@ public abstract class AbstractIdentityColumnMaxValueIncrementer extends Abstract private boolean deleteSpecificValues = false; /** The current cache of values. */ - private long[] valueCache; + private long @Nullable [] valueCache; /** The next id to serve from the value cache. */ private int nextValueIndex = -1; @@ -53,11 +56,9 @@ public abstract class AbstractIdentityColumnMaxValueIncrementer extends Abstract * @see #setIncrementerName * @see #setColumnName */ - @SuppressWarnings("NullAway") public AbstractIdentityColumnMaxValueIncrementer() { } - @SuppressWarnings("NullAway") public AbstractIdentityColumnMaxValueIncrementer(DataSource dataSource, String incrementerName, String columnName) { super(dataSource, incrementerName, columnName); } @@ -120,6 +121,7 @@ protected synchronized long getNextKey() throws DataAccessException { DataSourceUtils.releaseConnection(con, getDataSource()); } } + Assert.state(this.valueCache != null, "The cache of values can't be null"); return this.valueCache[this.nextValueIndex++]; } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java index dab17298db3f..4fc7bba19ab2 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/incrementer/package-info.java @@ -4,9 +4,7 @@ * *

    Can be used independently, for example in custom JDBC access code. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.support.incrementer; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java index d2ff13ec95ef..30d5490c1506 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/AbstractLobHandler.java @@ -21,7 +21,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract base class for {@link LobHandler} implementations. @@ -39,26 +39,22 @@ public abstract class AbstractLobHandler implements LobHandler { @Override - @Nullable - public byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException { + public byte @Nullable [] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException { return getBlobAsBytes(rs, rs.findColumn(columnName)); } @Override - @Nullable - public InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException { + public @Nullable InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException { return getBlobAsBinaryStream(rs, rs.findColumn(columnName)); } @Override - @Nullable - public String getClobAsString(ResultSet rs, String columnName) throws SQLException { + public @Nullable String getClobAsString(ResultSet rs, String columnName) throws SQLException { return getClobAsString(rs, rs.findColumn(columnName)); } @Override - @Nullable - public InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException { + public @Nullable InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException { return getClobAsAsciiStream(rs, rs.findColumn(columnName)); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java index 01fe9316befd..fc497a4f0ada 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/DefaultLobHandler.java @@ -30,8 +30,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Default implementation of the {@link LobHandler} interface. @@ -151,8 +150,7 @@ public void setCreateTemporaryLob(boolean createTemporaryLob) { @Override - @Nullable - public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException { + public byte @Nullable [] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException { logger.debug("Returning BLOB as bytes"); if (this.wrapAsLob) { Blob blob = rs.getBlob(columnIndex); @@ -164,8 +162,7 @@ public byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException } @Override - @Nullable - public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + public @Nullable InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { logger.debug("Returning BLOB as binary stream"); if (this.wrapAsLob) { Blob blob = rs.getBlob(columnIndex); @@ -177,8 +174,7 @@ public InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws S } @Override - @Nullable - public String getClobAsString(ResultSet rs, int columnIndex) throws SQLException { + public @Nullable String getClobAsString(ResultSet rs, int columnIndex) throws SQLException { logger.debug("Returning CLOB as string"); if (this.wrapAsLob) { Clob clob = rs.getClob(columnIndex); @@ -226,7 +222,7 @@ public LobCreator getLobCreator() { protected class DefaultLobCreator implements LobCreator { @Override - public void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content) + public void setBlobAsBytes(PreparedStatement ps, int paramIndex, byte @Nullable [] content) throws SQLException { if (streamAsLob) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java index 4c59328629aa..1b07cb22260a 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobCreator.java @@ -22,7 +22,7 @@ import java.sql.PreparedStatement; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface that abstracts potentially database-specific creation of large binary @@ -73,7 +73,7 @@ public interface LobCreator extends Closeable { * @throws SQLException if thrown by JDBC methods * @see java.sql.PreparedStatement#setBytes */ - void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content) + void setBlobAsBytes(PreparedStatement ps, int paramIndex, byte @Nullable [] content) throws SQLException; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java index 11fea7e50dec..1f852af0c969 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/LobHandler.java @@ -21,7 +21,7 @@ import java.sql.ResultSet; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstraction for handling large binary fields and large text fields in @@ -88,8 +88,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getBytes */ - @Nullable - byte[] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException; + byte @Nullable [] getBlobAsBytes(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as bytes from the given ResultSet. @@ -101,8 +100,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getBytes */ - @Nullable - byte[] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException; + byte @Nullable [] getBlobAsBytes(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as binary stream from the given ResultSet. @@ -114,8 +112,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getBinaryStream */ - @Nullable - InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException; + @Nullable InputStream getBlobAsBinaryStream(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as binary stream from the given ResultSet. @@ -127,8 +124,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getBinaryStream */ - @Nullable - InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; + @Nullable InputStream getBlobAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as String from the given ResultSet. @@ -140,8 +136,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getString */ - @Nullable - String getClobAsString(ResultSet rs, String columnName) throws SQLException; + @Nullable String getClobAsString(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as String from the given ResultSet. @@ -153,8 +148,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getString */ - @Nullable - String getClobAsString(ResultSet rs, int columnIndex) throws SQLException; + @Nullable String getClobAsString(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as ASCII stream from the given ResultSet. @@ -166,8 +160,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getAsciiStream */ - @Nullable - InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException; + @Nullable InputStream getClobAsAsciiStream(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as ASCII stream from the given ResultSet. @@ -179,8 +172,7 @@ public interface LobHandler { * @throws SQLException if thrown by JDBC methods * @see java.sql.ResultSet#getAsciiStream */ - @Nullable - InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException; + @Nullable InputStream getClobAsAsciiStream(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as character stream from the given ResultSet. 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 93238ea41ec0..0ed06daecf15 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. @@ -22,7 +22,7 @@ import java.sql.Blob; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Simple JDBC {@link Blob} adapter that exposes a given byte array or binary stream. @@ -31,14 +31,12 @@ * @author Juergen Hoeller * @since 2.5.3 */ -@Deprecated +@Deprecated(since = "6.2") class PassThroughBlob implements Blob { - @Nullable - private byte[] content; + private byte @Nullable [] content; - @Nullable - private InputStream binaryStream; + private @Nullable InputStream binaryStream; private final long contentLength; 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 8dea8a2c842a..47324befff23 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. @@ -28,7 +28,8 @@ import java.sql.Clob; import java.sql.SQLException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.FileCopyUtils; /** @@ -38,17 +39,14 @@ * @author Juergen Hoeller * @since 2.5.3 */ -@Deprecated +@Deprecated(since = "6.2") class PassThroughClob implements Clob { - @Nullable - private String content; + private @Nullable String content; - @Nullable - private Reader characterStream; + private @Nullable Reader characterStream; - @Nullable - private InputStream asciiStream; + private @Nullable InputStream asciiStream; private final long contentLength; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java index 7a0e18db9ca3..820ca7b8c284 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/TemporaryLobCreator.java @@ -28,9 +28,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; import org.springframework.util.FileCopyUtils; /** @@ -60,7 +60,7 @@ public class TemporaryLobCreator implements LobCreator { @Override - public void setBlobAsBytes(PreparedStatement ps, int paramIndex, @Nullable byte[] content) + public void setBlobAsBytes(PreparedStatement ps, int paramIndex, byte @Nullable [] content) throws SQLException { if (content != null) { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java index a07f8dac5d02..85525271fe83 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/lob/package-info.java @@ -2,9 +2,7 @@ * Provides a strategy interface for Large OBject handling, * as well as a customizable default implementation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.support.lob; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java index bd868877a9af..44d0b871a4a3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/package-info.java @@ -6,9 +6,7 @@ *

    Can be used independently, for example in custom JDBC access code, * or in JDBC-based O/R mapping layers. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java index 04d90e000cba..9f3c0feb9595 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSet.java @@ -27,8 +27,9 @@ import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.InvalidResultSetAccessException; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; @@ -173,8 +174,7 @@ public int findColumn(String columnLabel) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getBigDecimal(int) */ @Override - @Nullable - public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getBigDecimal(columnIndex); } @@ -187,8 +187,7 @@ public BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessEx * @see java.sql.ResultSet#getBigDecimal(String) */ @Override - @Nullable - public BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException { return getBigDecimal(findColumn(columnLabel)); } @@ -238,8 +237,7 @@ public byte getByte(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getDate(int) */ @Override - @Nullable - public Date getDate(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable Date getDate(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getDate(columnIndex); } @@ -252,8 +250,7 @@ public Date getDate(int columnIndex) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getDate(String) */ @Override - @Nullable - public Date getDate(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable Date getDate(String columnLabel) throws InvalidResultSetAccessException { return getDate(findColumn(columnLabel)); } @@ -261,8 +258,7 @@ public Date getDate(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getDate(int, Calendar) */ @Override - @Nullable - public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + public @Nullable Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { try { return this.resultSet.getDate(columnIndex, cal); } @@ -275,8 +271,7 @@ public Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccess * @see java.sql.ResultSet#getDate(String, Calendar) */ @Override - @Nullable - public Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { + public @Nullable Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { return getDate(findColumn(columnLabel), cal); } @@ -368,8 +363,7 @@ public long getLong(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getNString(int) */ @Override - @Nullable - public String getNString(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable String getNString(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getNString(columnIndex); } @@ -382,8 +376,7 @@ public String getNString(int columnIndex) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getNString(String) */ @Override - @Nullable - public String getNString(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable String getNString(String columnLabel) throws InvalidResultSetAccessException { return getNString(findColumn(columnLabel)); } @@ -391,8 +384,7 @@ public String getNString(String columnLabel) throws InvalidResultSetAccessExcept * @see java.sql.ResultSet#getObject(int) */ @Override - @Nullable - public Object getObject(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable Object getObject(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getObject(columnIndex); } @@ -405,8 +397,7 @@ public Object getObject(int columnIndex) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getObject(String) */ @Override - @Nullable - public Object getObject(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable Object getObject(String columnLabel) throws InvalidResultSetAccessException { return getObject(findColumn(columnLabel)); } @@ -414,8 +405,7 @@ public Object getObject(String columnLabel) throws InvalidResultSetAccessExcepti * @see java.sql.ResultSet#getObject(int, Map) */ @Override - @Nullable - public Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException { + public @Nullable Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException { try { return this.resultSet.getObject(columnIndex, map); } @@ -428,8 +418,7 @@ public Object getObject(int columnIndex, Map> map) throws Inval * @see java.sql.ResultSet#getObject(String, Map) */ @Override - @Nullable - public Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException { + public @Nullable Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException { return getObject(findColumn(columnLabel), map); } @@ -437,8 +426,7 @@ public Object getObject(String columnLabel, Map> map) throws In * @see java.sql.ResultSet#getObject(int, Class) */ @Override - @Nullable - public T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException { + public @Nullable T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException { try { return this.resultSet.getObject(columnIndex, type); } @@ -451,8 +439,7 @@ public T getObject(int columnIndex, Class type) throws InvalidResultSetAc * @see java.sql.ResultSet#getObject(String, Class) */ @Override - @Nullable - public T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException { + public @Nullable T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException { return getObject(findColumn(columnLabel), type); } @@ -481,8 +468,7 @@ public short getShort(String columnLabel) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getString(int) */ @Override - @Nullable - public String getString(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable String getString(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getString(columnIndex); } @@ -495,8 +481,7 @@ public String getString(int columnIndex) throws InvalidResultSetAccessException * @see java.sql.ResultSet#getString(String) */ @Override - @Nullable - public String getString(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable String getString(String columnLabel) throws InvalidResultSetAccessException { return getString(findColumn(columnLabel)); } @@ -504,8 +489,7 @@ public String getString(String columnLabel) throws InvalidResultSetAccessExcepti * @see java.sql.ResultSet#getTime(int) */ @Override - @Nullable - public Time getTime(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable Time getTime(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getTime(columnIndex); } @@ -518,8 +502,7 @@ public Time getTime(int columnIndex) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getTime(String) */ @Override - @Nullable - public Time getTime(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable Time getTime(String columnLabel) throws InvalidResultSetAccessException { return getTime(findColumn(columnLabel)); } @@ -527,8 +510,7 @@ public Time getTime(String columnLabel) throws InvalidResultSetAccessException { * @see java.sql.ResultSet#getTime(int, Calendar) */ @Override - @Nullable - public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + public @Nullable Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { try { return this.resultSet.getTime(columnIndex, cal); } @@ -541,8 +523,7 @@ public Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccess * @see java.sql.ResultSet#getTime(String, Calendar) */ @Override - @Nullable - public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { + public @Nullable Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { return getTime(findColumn(columnLabel), cal); } @@ -550,8 +531,7 @@ public Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAcc * @see java.sql.ResultSet#getTimestamp(int) */ @Override - @Nullable - public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException { + public @Nullable Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException { try { return this.resultSet.getTimestamp(columnIndex); } @@ -564,8 +544,7 @@ public Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessExce * @see java.sql.ResultSet#getTimestamp(String) */ @Override - @Nullable - public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException { + public @Nullable Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException { return getTimestamp(findColumn(columnLabel)); } @@ -573,8 +552,7 @@ public Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessE * @see java.sql.ResultSet#getTimestamp(int, Calendar) */ @Override - @Nullable - public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { + public @Nullable Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException { try { return this.resultSet.getTimestamp(columnIndex, cal); } @@ -587,8 +565,7 @@ public Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResul * @see java.sql.ResultSet#getTimestamp(String, Calendar) */ @Override - @Nullable - public Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { + public @Nullable Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException { return getTimestamp(findColumn(columnLabel), cal); } diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java index 88ad0941f593..8e8decbe1f4e 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/ResultSetWrappingSqlRowSetMetaData.java @@ -19,8 +19,9 @@ import java.sql.ResultSetMetaData; import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.InvalidResultSetAccessException; -import org.springframework.lang.Nullable; /** * The default implementation of Spring's {@link SqlRowSetMetaData} interface, wrapping a @@ -38,8 +39,7 @@ public class ResultSetWrappingSqlRowSetMetaData implements SqlRowSetMetaData { private final ResultSetMetaData resultSetMetaData; - @Nullable - private String[] columnNames; + private String @Nullable [] columnNames; /** diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java index a460f8154004..1de3e7d24fe0 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/SqlRowSet.java @@ -24,8 +24,9 @@ import java.util.Calendar; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.jdbc.InvalidResultSetAccessException; -import org.springframework.lang.Nullable; /** * Mirror interface for {@link javax.sql.RowSet}, representing a disconnected variant of @@ -75,8 +76,7 @@ public interface SqlRowSet extends Serializable { * @return an BigDecimal object representing the column value * @see java.sql.ResultSet#getBigDecimal(int) */ - @Nullable - BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException; + @Nullable BigDecimal getBigDecimal(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a BigDecimal object. @@ -84,8 +84,7 @@ public interface SqlRowSet extends Serializable { * @return an BigDecimal object representing the column value * @see java.sql.ResultSet#getBigDecimal(String) */ - @Nullable - BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException; + @Nullable BigDecimal getBigDecimal(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a boolean. @@ -125,8 +124,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(int) */ - @Nullable - Date getDate(int columnIndex) throws InvalidResultSetAccessException; + @Nullable Date getDate(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Date object. @@ -134,8 +132,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(String) */ - @Nullable - Date getDate(String columnLabel) throws InvalidResultSetAccessException; + @Nullable Date getDate(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Date object. @@ -144,8 +141,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(int, Calendar) */ - @Nullable - Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + @Nullable Date getDate(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Date object. @@ -154,8 +150,7 @@ public interface SqlRowSet extends Serializable { * @return a Date object representing the column value * @see java.sql.ResultSet#getDate(String, Calendar) */ - @Nullable - Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; + @Nullable Date getDate(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Double object. @@ -229,8 +224,7 @@ public interface SqlRowSet extends Serializable { * @since 4.1.3 * @see java.sql.ResultSet#getNString(int) */ - @Nullable - String getNString(int columnIndex) throws InvalidResultSetAccessException; + @Nullable String getNString(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a String @@ -240,8 +234,7 @@ public interface SqlRowSet extends Serializable { * @since 4.1.3 * @see java.sql.ResultSet#getNString(String) */ - @Nullable - String getNString(String columnLabel) throws InvalidResultSetAccessException; + @Nullable String getNString(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as an Object. @@ -249,8 +242,7 @@ public interface SqlRowSet extends Serializable { * @return an Object representing the column value * @see java.sql.ResultSet#getObject(int) */ - @Nullable - Object getObject(int columnIndex) throws InvalidResultSetAccessException; + @Nullable Object getObject(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as an Object. @@ -258,8 +250,7 @@ public interface SqlRowSet extends Serializable { * @return an Object representing the column value * @see java.sql.ResultSet#getObject(String) */ - @Nullable - Object getObject(String columnLabel) throws InvalidResultSetAccessException; + @Nullable Object getObject(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as an Object. @@ -268,8 +259,7 @@ public interface SqlRowSet extends Serializable { * @return an Object representing the column value * @see java.sql.ResultSet#getObject(int, Map) */ - @Nullable - Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException; + @Nullable Object getObject(int columnIndex, Map> map) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as an Object. @@ -278,8 +268,7 @@ public interface SqlRowSet extends Serializable { * @return an Object representing the column value * @see java.sql.ResultSet#getObject(String, Map) */ - @Nullable - Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException; + @Nullable Object getObject(String columnLabel, Map> map) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as an Object. @@ -289,8 +278,7 @@ public interface SqlRowSet extends Serializable { * @since 4.1.3 * @see java.sql.ResultSet#getObject(int, Class) */ - @Nullable - T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException; + @Nullable T getObject(int columnIndex, Class type) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as an Object. @@ -300,8 +288,7 @@ public interface SqlRowSet extends Serializable { * @since 4.1.3 * @see java.sql.ResultSet#getObject(String, Class) */ - @Nullable - T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException; + @Nullable T getObject(String columnLabel, Class type) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a short. @@ -325,8 +312,7 @@ public interface SqlRowSet extends Serializable { * @return a String representing the column value * @see java.sql.ResultSet#getString(int) */ - @Nullable - String getString(int columnIndex) throws InvalidResultSetAccessException; + @Nullable String getString(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a String. @@ -334,8 +320,7 @@ public interface SqlRowSet extends Serializable { * @return a String representing the column value * @see java.sql.ResultSet#getString(String) */ - @Nullable - String getString(String columnLabel) throws InvalidResultSetAccessException; + @Nullable String getString(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Time object. @@ -343,8 +328,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(int) */ - @Nullable - Time getTime(int columnIndex) throws InvalidResultSetAccessException; + @Nullable Time getTime(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Time object. @@ -352,8 +336,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(String) */ - @Nullable - Time getTime(String columnLabel) throws InvalidResultSetAccessException; + @Nullable Time getTime(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Time object. @@ -362,8 +345,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(int, Calendar) */ - @Nullable - Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + @Nullable Time getTime(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Time object. @@ -372,8 +354,7 @@ public interface SqlRowSet extends Serializable { * @return a Time object representing the column value * @see java.sql.ResultSet#getTime(String, Calendar) */ - @Nullable - Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; + @Nullable Time getTime(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Timestamp object. @@ -381,8 +362,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(int) */ - @Nullable - Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException; + @Nullable Timestamp getTimestamp(int columnIndex) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Timestamp object. @@ -390,8 +370,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(String) */ - @Nullable - Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException; + @Nullable Timestamp getTimestamp(String columnLabel) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Timestamp object. @@ -400,8 +379,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(int, Calendar) */ - @Nullable - Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; + @Nullable Timestamp getTimestamp(int columnIndex, Calendar cal) throws InvalidResultSetAccessException; /** * Retrieve the value of the indicated column in the current row as a Timestamp object. @@ -410,8 +388,7 @@ public interface SqlRowSet extends Serializable { * @return a Timestamp object representing the column value * @see java.sql.ResultSet#getTimestamp(String, Calendar) */ - @Nullable - Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; + @Nullable Timestamp getTimestamp(String columnLabel, Calendar cal) throws InvalidResultSetAccessException; // RowSet navigation methods diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java index d38a51b1c694..4481066ea3eb 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/rowset/package-info.java @@ -2,9 +2,7 @@ * Provides a convenient holder for disconnected result sets. * Supported by JdbcTemplate, but can be used independently too. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.support.rowset; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java index 9a5775c71206..75c1807cfde8 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/Jdbc4SqlXmlHandler.java @@ -30,10 +30,10 @@ import javax.xml.transform.dom.DOMResult; import javax.xml.transform.dom.DOMSource; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; /** * Default implementation of the {@link SqlXmlHandler} interface. @@ -59,50 +59,43 @@ public class Jdbc4SqlXmlHandler implements SqlXmlHandler { //------------------------------------------------------------------------- @Override - @Nullable - public String getXmlAsString(ResultSet rs, String columnName) throws SQLException { + public @Nullable String getXmlAsString(ResultSet rs, String columnName) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnName); return (xmlObject != null ? xmlObject.getString() : null); } @Override - @Nullable - public String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException { + public @Nullable String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnIndex); return (xmlObject != null ? xmlObject.getString() : null); } @Override - @Nullable - public InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException { + public @Nullable InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnName); return (xmlObject != null ? xmlObject.getBinaryStream() : null); } @Override - @Nullable - public InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { + public @Nullable InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnIndex); return (xmlObject != null ? xmlObject.getBinaryStream() : null); } @Override - @Nullable - public Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException { + public @Nullable Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnName); return (xmlObject != null ? xmlObject.getCharacterStream() : null); } @Override - @Nullable - public Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { + public @Nullable Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnIndex); return (xmlObject != null ? xmlObject.getCharacterStream() : null); } @Override - @Nullable - public Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) + public @Nullable Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnName); @@ -113,8 +106,7 @@ public Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) + public @Nullable Source getXmlAsSource(ResultSet rs, int columnIndex, @Nullable Class sourceClass) throws SQLException { SQLXML xmlObject = rs.getSQLXML(columnIndex); @@ -185,8 +177,7 @@ protected void provideXml(SQLXML xmlObject) throws SQLException, IOException { */ private abstract static class AbstractJdbc4SqlXmlValue implements SqlXmlValue { - @Nullable - private SQLXML xmlObject; + private @Nullable SQLXML xmlObject; @Override public void setValue(PreparedStatement ps, int paramIndex) throws SQLException { diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java index de37b3b95b39..e9fd4304fafc 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/SqlXmlHandler.java @@ -25,10 +25,9 @@ import javax.xml.transform.Result; import javax.xml.transform.Source; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; -import org.springframework.lang.Nullable; - /** * Abstraction for handling XML fields in specific databases. Its main purpose * is to isolate database-specific handling of XML stored in the database. @@ -69,8 +68,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getString * @see java.sql.ResultSet#getSQLXML */ - @Nullable - String getXmlAsString(ResultSet rs, String columnName) throws SQLException; + @Nullable String getXmlAsString(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as String from the given ResultSet. @@ -84,8 +82,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getString * @see java.sql.ResultSet#getSQLXML */ - @Nullable - String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException; + @Nullable String getXmlAsString(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as binary stream from the given ResultSet. @@ -99,8 +96,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getSQLXML * @see java.sql.SQLXML#getBinaryStream */ - @Nullable - InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException; + @Nullable InputStream getXmlAsBinaryStream(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as binary stream from the given ResultSet. @@ -114,8 +110,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getSQLXML * @see java.sql.SQLXML#getBinaryStream */ - @Nullable - InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; + @Nullable InputStream getXmlAsBinaryStream(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as character stream from the given ResultSet. @@ -129,8 +124,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getSQLXML * @see java.sql.SQLXML#getCharacterStream */ - @Nullable - Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException; + @Nullable Reader getXmlAsCharacterStream(ResultSet rs, String columnName) throws SQLException; /** * Retrieve the given column as character stream from the given ResultSet. @@ -144,8 +138,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getSQLXML * @see java.sql.SQLXML#getCharacterStream */ - @Nullable - Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; + @Nullable Reader getXmlAsCharacterStream(ResultSet rs, int columnIndex) throws SQLException; /** * Retrieve the given column as Source implemented using the specified source class @@ -160,8 +153,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getSQLXML * @see java.sql.SQLXML#getSource */ - @Nullable - Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) throws SQLException; + @Nullable Source getXmlAsSource(ResultSet rs, String columnName, @Nullable Class sourceClass) throws SQLException; /** * Retrieve the given column as Source implemented using the specified source class @@ -176,8 +168,7 @@ public interface SqlXmlHandler { * @see java.sql.ResultSet#getSQLXML * @see java.sql.SQLXML#getSource */ - @Nullable - Source getXmlAsSource(ResultSet rs, int columnIndex, @Nullable Class sourceClass) throws SQLException; + @Nullable Source getXmlAsSource(ResultSet rs, int columnIndex, @Nullable Class sourceClass) throws SQLException; //------------------------------------------------------------------------- diff --git a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java index 24e863bfd6ae..d0553a560df3 100644 --- a/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java +++ b/spring-jdbc/src/main/java/org/springframework/jdbc/support/xml/package-info.java @@ -1,9 +1,7 @@ /** * Abstraction for handling fields of SQLXML data type. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jdbc.support.xml; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 1a7b808c23fb..e29bb0f01b66 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. @@ -25,7 +25,7 @@ import java.sql.ResultSet * @since 5.0 */ inline fun JdbcOperations.queryForObject(sql: String): T = - queryForObject(sql, T::class.java) as T + queryForObject(sql, T::class.java as Class<*>) as T /** * Extensions for [JdbcOperations.queryForObject] providing a RowMapper-like function @@ -45,7 +45,7 @@ inline fun JdbcOperations.queryForObject(sql: String, vararg args: A * @since 5.0 */ inline fun JdbcOperations.queryForObject(sql: String, args: Array, argTypes: IntArray): T? = - queryForObject(sql, args, argTypes, T::class.java) as T + queryForObject(sql, args, argTypes, T::class.java as Class<*>) as T /** * Extension for [JdbcOperations.queryForObject] providing a @@ -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 T + queryForObject(sql, T::class.java as Class<*>, args) as T /** * Extension for [JdbcOperations.queryForList] providing a `queryForList("...")` variant. @@ -66,7 +64,7 @@ inline fun JdbcOperations.queryForObject(sql: String, args: Array JdbcOperations.queryForList(sql: String): List = +inline fun JdbcOperations.queryForList(sql: String): List = queryForList(sql, T::class.java) /** @@ -77,7 +75,7 @@ inline fun JdbcOperations.queryForList(sql: String): List = * @since 5.0 */ @Suppress("EXTENSION_SHADOWED_BY_MEMBER") -inline fun JdbcOperations.queryForList(sql: String, args: Array, +inline fun JdbcOperations.queryForList(sql: String, args: Array, argTypes: IntArray): List = queryForList(sql, args, argTypes, T::class.java) @@ -88,10 +86,8 @@ inline fun JdbcOperations.queryForList(sql: String, args: Array JdbcOperations.queryForList(sql: String, args: Array): List = - queryForList(sql, args, T::class.java) +inline fun JdbcOperations.queryForList(sql: String, args: Array): List = + queryForList(sql, T::class.java, args) /** diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java index e03b2d336152..bc1065d4aaa2 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/config/JdbcNamespaceIntegrationTests.java @@ -18,6 +18,7 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.PropertyValue; @@ -33,7 +34,6 @@ import org.springframework.jdbc.datasource.AbstractDriverBasedDataSource; import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactoryBean; import org.springframework.jdbc.datasource.init.DataSourceInitializer; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; @@ -188,8 +188,7 @@ private void assertCorrectSetupAndCloseContext(String file, int count, String... } } - @Nullable - private String extractDataSourceUrl(String file) { + private @Nullable String extractDataSourceUrl(String file) { try (ConfigurableApplicationContext context = context(file)) { DataSource dataSource = context.getBean(DataSource.class); assertNumRowsInTestTable(new JdbcTemplate(dataSource), 1); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.java index b46065d3c63f..01f4143372f1 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/StatementCreatorUtilsTests.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,8 +38,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Named.named; -import static org.mockito.BDDMockito.doThrow; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java index a6c318ef1a38..db12eeeec8ee 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/metadata/GenericCallMetaDataProviderTests.java @@ -24,11 +24,11 @@ import java.util.List; import java.util.function.IntFunction; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.InOrder; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; import org.springframework.util.function.ThrowingBiFunction; import static org.assertj.core.api.Assertions.assertThat; 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 99daa91494a2..84c2f7981f98 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 @@ void updateWithGeneratedKeysAndKeyColumnNamesUsingNamedParameters() { } + @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/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java index a8212b4a6931..e2da04ddc12c 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/JdbcClientQueryTests.java @@ -16,10 +16,12 @@ package org.springframework.jdbc.core.simple; +import java.math.BigInteger; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.ResultSet; import java.sql.ResultSetMetaData; +import java.sql.SQLFeatureNotSupportedException; import java.sql.Types; import java.util.ArrayList; import java.util.Arrays; @@ -33,6 +35,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.core.convert.converter.Converter; +import org.springframework.core.convert.support.GenericConversionService; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.util.NumberUtils; + import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.mockito.ArgumentMatchers.anyString; @@ -593,13 +600,22 @@ void queryForMappedRecordWithNamedParam() throws Exception { @Test void queryForMappedFieldHolderWithNamedParam() throws Exception { given(resultSet.next()).willReturn(true, false); - given(resultSet.getInt(1)).willReturn(22); - + given(resultSet.getObject(1, BigInteger.class)).willThrow(new SQLFeatureNotSupportedException()); + given(resultSet.getObject(1)).willReturn("big22"); + + GenericConversionService conversionService = new GenericConversionService(); + conversionService.addConverter(new Converter() { // explicit for generics + @Override + public BigInteger convert(String source) { + return NumberUtils.parseNumber(source.substring(3), BigInteger.class); + } + }); + client = JdbcClient.create(new NamedParameterJdbcTemplate(dataSource), conversionService); AgeFieldHolder value = client.sql("SELECT AGE FROM CUSTMR WHERE ID = :id") .param("id", 3) .query(AgeFieldHolder.class).single(); - assertThat(value.age).isEqualTo(22); + assertThat(value.age).isEqualTo(BigInteger.valueOf(22)); verify(connection).prepareStatement("SELECT AGE FROM CUSTMR WHERE ID = ?"); verify(preparedStatement).setObject(1, 3); verify(resultSet).close(); @@ -656,7 +672,7 @@ record AgeRecord(int age) { static class AgeFieldHolder { - public int age; + public BigInteger age; } } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java index 9d4342a78575..064928f1dc6a 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/core/simple/SimpleJdbcCallTests.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,17 +25,20 @@ import javax.sql.DataSource; +import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.BadSqlGrammarException; +import org.springframework.jdbc.core.RowMapper; import org.springframework.jdbc.core.SqlOutParameter; import org.springframework.jdbc.core.SqlParameter; import org.springframework.jdbc.core.namedparam.MapSqlParameterSource; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; @@ -360,4 +363,37 @@ void correctSybaseFunctionStatementNamed() throws Exception { verifyStatement(adder, "{call ADD_INVOICE(@AMOUNT = ?, @CUSTID = ?)}"); } + @Test + void declareParametersCannotBeInvokedWhenCompiled() { + SimpleJdbcCall call = new SimpleJdbcCall(dataSource) + .withProcedureName("procedure_name") + .declareParameters(new SqlParameter("PARAM", Types.VARCHAR)); + call.compile(); + assertThatIllegalStateException() + .isThrownBy(() -> call.declareParameters(new SqlParameter("Ignored Param", Types.VARCHAR))) + .withMessage("SqlCall for procedure is already compiled"); + } + + @Test + void addDeclaredRowMapperCanBeConfigured() { + SimpleJdbcCall call = new SimpleJdbcCall(dataSource) + .withProcedureName("procedure_name") + .returningResultSet("result_set", (rs, i) -> new Object()); + + assertThat(call).extracting("declaredRowMappers") + .asInstanceOf(InstanceOfAssertFactories.map(String.class, RowMapper.class)) + .containsOnlyKeys("result_set"); + } + + @Test + void addDeclaredRowMapperWhenCompiled() { + SimpleJdbcCall call = new SimpleJdbcCall(dataSource) + .withProcedureName("procedure_name") + .returningResultSet("result_set", (rs, i) -> new Object()); + call.compile(); + assertThatIllegalStateException() + .isThrownBy(() -> call.returningResultSet("not added", (rs, i) -> new Object())) + .withMessage("SqlCall for procedure is already compiled"); + } + } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceUtilsTests.java index 36bd1fc250fc..286095e32be0 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/DataSourceUtilsTests.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. @@ -25,8 +25,8 @@ import org.springframework.jdbc.CannotGetJdbcConnectionException; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link DataSourceUtils}. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapterTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapterTests.java index 8f4261547b66..ba617dcedc3b 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapterTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/ShardingKeyDataSourceAdapterTests.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,10 +27,10 @@ import org.junit.jupiter.api.Test; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.RETURNS_DEEP_STUBS; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.RETURNS_DEEP_STUBS; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link ShardingKeyDataSourceAdapter}. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHintsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHintsTests.java index 2bfb57b7670c..76853ad7b583 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHintsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/embedded/EmbeddedDatabaseFactoryRuntimeHintsTests.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. @@ -47,7 +47,7 @@ void setup() { @Test void embeddedDataSourceProxyTypeHasHint() throws ClassNotFoundException { assertThat(RuntimeHintsPredicates.reflection() - .onMethod("org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory$EmbeddedDataSourceProxy", "shutdown")) + .onMethodInvocation("org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory$EmbeddedDataSourceProxy", "shutdown")) .accepts(this.hints); } diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.java index 17d125dbb04a..1a2dce315494 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ResourceDatabasePopulatorTests.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,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.mock; +import static org.mockito.Mockito.mock; /** * Tests for {@link ResourceDatabasePopulator}. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsTests.java index 1872fbe522d8..6b4243dd4782 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/datasource/init/ScriptUtilsTests.java @@ -20,13 +20,13 @@ import java.util.List; import org.assertj.core.util.Strings; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.springframework.jdbc.datasource.init.ScriptUtils.DEFAULT_BLOCK_COMMENT_END_DELIMITER; diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java index 372ed627cb9d..19257b222803 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/object/SqlQueryTests.java @@ -26,9 +26,11 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Stream; import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -36,7 +38,6 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.jdbc.Customer; import org.springframework.jdbc.core.SqlParameter; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -52,10 +53,10 @@ * @author Trevor Cook * @author Thomas Risberg * @author Juergen Hoeller + * @author Yanming Zhou */ class SqlQueryTests { - // FIXME inline? private static final String SELECT_ID = "select id from custmr"; private static final String SELECT_ID_WHERE = @@ -108,7 +109,7 @@ void testQueryWithoutParams() throws SQLException { SqlQuery query = new MappingSqlQueryWithParameters<>() { @Override - protected Integer mapRow(ResultSet rs, int rownum, @Nullable Object[] params, @Nullable Map context) + protected Integer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map context) throws SQLException { assertThat(params).as("params were null").isNull(); assertThat(context).as("context was null").isNull(); @@ -126,6 +127,72 @@ protected Integer mapRow(ResultSet rs, int rownum, @Nullable Object[] params, @N verify(preparedStatement).close(); } + @Test + void testStreamWithoutParams() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt(1)).willReturn(1); + + SqlQuery query = new MappingSqlQueryWithParameters<>() { + @Override + protected Integer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map context) + throws SQLException { + assertThat(params).as("params were null").isNull(); + assertThat(context).as("context was null").isNull(); + return rs.getInt(1); + } + }; + query.setDataSource(dataSource); + query.setSql(SELECT_ID); + query.compile(); + try (Stream stream = query.stream()) { + List list = stream.toList(); + assertThat(list).containsExactly(1); + } + verify(connection).prepareStatement(SELECT_ID); + verify(resultSet).close(); + verify(preparedStatement).close(); + } + + @Test + void testStreamByNamedParam() throws SQLException { + given(resultSet.next()).willReturn(true, false); + given(resultSet.getInt("id")).willReturn(1); + given(resultSet.getString("forename")).willReturn("rod"); + given(connection.prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED, + ResultSet.TYPE_SCROLL_SENSITIVE, ResultSet.CONCUR_READ_ONLY) + ).willReturn(preparedStatement); + + SqlQuery query = new MappingSqlQueryWithParameters<>() { + @Override + protected Customer mapRow(ResultSet rs, int rownum, Object @Nullable [] params, @Nullable Map context) + throws SQLException { + assertThat(params).as("params were not null").isNotNull(); + assertThat(context).as("context was null").isNull(); + Customer cust = new Customer(); + cust.setId(rs.getInt(COLUMN_NAMES[0])); + cust.setForename(rs.getString(COLUMN_NAMES[1])); + return cust; + } + }; + query.declareParameter(new SqlParameter("id", Types.NUMERIC)); + query.declareParameter(new SqlParameter("country", Types.VARCHAR)); + query.setDataSource(dataSource); + query.setSql(SELECT_ID_FORENAME_NAMED_PARAMETERS); + query.compile(); + try (Stream stream = query.streamByNamedParam(Map.of("id", 1, "country", "UK"))) { + List list = stream.toList(); + assertThat(list).hasSize(1); + Customer customer = list.get(0); + assertThat(customer.getId()).isEqualTo(1); + assertThat(customer.getForename()).isEqualTo("rod"); + } + verify(connection).prepareStatement(SELECT_ID_FORENAME_NAMED_PARAMETERS_PARSED); + verify(preparedStatement).setObject(1, 1, Types.NUMERIC); + verify(preparedStatement).setString(2, "UK"); + verify(resultSet).close(); + verify(preparedStatement).close(); + } + @Test void testQueryWithoutEnoughParams() { MappingSqlQuery query = new MappingSqlQuery<>() { @@ -163,13 +230,11 @@ protected Integer mapRow(ResultSet rs, int rownum) throws SQLException { } @Test - @SuppressWarnings("removal") void testStringQueryWithResults() throws Exception { String[] dbResults = new String[] { "alpha", "beta", "charlie" }; given(resultSet.next()).willReturn(true, true, true, false); given(resultSet.getString(1)).willReturn(dbResults[0], dbResults[1], dbResults[2]); StringQuery query = new StringQuery(dataSource, SELECT_FORENAME); - query.setRowsExpected(3); String[] results = query.run(); assertThat(results).isEqualTo(dbResults); verify(connection).prepareStatement(SELECT_FORENAME); diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java index 1a5d833de0b7..750351ea0fff 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/CustomSqlExceptionTranslator.java @@ -18,9 +18,10 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; import org.springframework.dao.TransientDataAccessResourceException; -import org.springframework.lang.Nullable; /** * Custom SQLException translation for testing. diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java index 76ec0720b32b..4fc8995646d7 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLErrorCodeSQLExceptionTranslatorTests.java @@ -24,6 +24,7 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.dao.CannotAcquireLockException; @@ -33,7 +34,6 @@ import org.springframework.dao.DuplicateKeyException; import org.springframework.jdbc.BadSqlGrammarException; import org.springframework.jdbc.InvalidResultSetAccessException; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -139,8 +139,7 @@ void customTranslateMethodTranslation() { translator = new SQLErrorCodeSQLExceptionTranslator() { @SuppressWarnings("deprecation") @Override - @Nullable - protected DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlException) { + protected @Nullable DataAccessException customTranslate(String task, @Nullable String sql, SQLException sqlException) { assertThat(task).isEqualTo(TASK); assertThat(sql).isEqualTo(SQL); return (sqlException == badSqlEx) ? customDex : null; diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java index 28758b874560..b3156a7073ca 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SQLStateSQLExceptionTranslatorTests.java @@ -18,6 +18,7 @@ import java.sql.SQLException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.dao.CannotAcquireLockException; @@ -28,7 +29,6 @@ import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.jdbc.BadSqlGrammarException; -import org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; diff --git a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SqlArrayValueTests.java b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SqlArrayValueTests.java index 10fbebeb0751..141713efcba1 100644 --- a/spring-jdbc/src/test/java/org/springframework/jdbc/support/SqlArrayValueTests.java +++ b/spring-jdbc/src/test/java/org/springframework/jdbc/support/SqlArrayValueTests.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,8 +24,8 @@ import org.junit.jupiter.api.Test; import static org.mockito.BDDMockito.given; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.verify; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; /** * Tests for {@link SqlArrayValue}. 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 e3e086b30f6d..894033e9bb28 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. @@ -16,13 +16,13 @@ package org.springframework.jdbc.core -import java.sql.* - import io.mockk.every import io.mockk.mockk import io.mockk.verify import org.assertj.core.api.Assertions.assertThat import org.junit.jupiter.api.Test +import java.sql.JDBCType +import java.sql.ResultSet /** * Mock object based tests for [JdbcOperations] Kotlin extensions @@ -54,7 +54,7 @@ class JdbcOperationsExtensionsTests { fun `queryForObject with nullable RowMapper-like function`() { every { template.queryForObject(sql, any>(), 3) } returns null assertThat(template.queryForObject(sql, 3) { _, _ -> null }).isNull() - verify { template.queryForObject(eq(sql), any>(), eq(3)) } + verify { template.queryForObject(eq(sql), any>(), eq(3)) } } @Test @@ -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 @@ -115,9 +113,9 @@ class JdbcOperationsExtensionsTests { @Test // gh-22682 fun `query with nullable ResultSetExtractor-like function`() { - every { template.query(eq(sql), any>(), eq(3)) } returns null + every { template.query(eq(sql), any>(), eq(3)) } returns null assertThat(template.query(sql, 3) { _ -> null }).isNull() - verify { template.query(eq(sql), any>(), eq(3)) } + verify { template.query(eq(sql), any>(), eq(3)) } } @Suppress("RemoveExplicitTypeArguments") diff --git a/spring-jms/spring-jms.gradle b/spring-jms/spring-jms.gradle index 31da2fb30245..9014b09663da 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/JmsException.java b/spring-jms/src/main/java/org/springframework/jms/JmsException.java index f8a74a4216f7..b8dd071e4d6f 100644 --- a/spring-jms/src/main/java/org/springframework/jms/JmsException.java +++ b/spring-jms/src/main/java/org/springframework/jms/JmsException.java @@ -17,9 +17,9 @@ package org.springframework.jms; import jakarta.jms.JMSException; +import org.jspecify.annotations.Nullable; import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * Base class for exception thrown by the framework whenever it @@ -68,8 +68,7 @@ public JmsException(@Nullable Throwable cause) { * @return a string specifying the vendor-specific error code if the * root cause is an instance of JMSException, or {@code null} */ - @Nullable - public String getErrorCode() { + public @Nullable String getErrorCode() { Throwable cause = getCause(); if (cause instanceof JMSException jmsException) { return jmsException.getErrorCode(); @@ -83,8 +82,7 @@ public String getErrorCode() { * @see jakarta.jms.JMSException#getLinkedException() */ @Override - @Nullable - public String getMessage() { + public @Nullable String getMessage() { String message = super.getMessage(); Throwable cause = getCause(); if (cause instanceof JMSException jmsException) { diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java index d35c228f226a..319069bd7035 100644 --- a/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/JmsListenerAnnotationBeanPostProcessor.java @@ -27,6 +27,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.AopInfrastructureBean; import org.springframework.aop.framework.AopProxyUtils; @@ -52,7 +53,6 @@ import org.springframework.jms.config.JmsListenerEndpointRegistrar; import org.springframework.jms.config.JmsListenerEndpointRegistry; import org.springframework.jms.config.MethodJmsListenerEndpoint; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; @@ -99,20 +99,16 @@ public class JmsListenerAnnotationBeanPostProcessor protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private String containerFactoryBeanName = DEFAULT_JMS_LISTENER_CONTAINER_FACTORY_BEAN_NAME; + private @Nullable String containerFactoryBeanName = DEFAULT_JMS_LISTENER_CONTAINER_FACTORY_BEAN_NAME; - @Nullable - private JmsListenerEndpointRegistry endpointRegistry; + private @Nullable JmsListenerEndpointRegistry endpointRegistry; private final MessageHandlerMethodFactoryAdapter messageHandlerMethodFactory = new MessageHandlerMethodFactoryAdapter(); - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; private final JmsListenerEndpointRegistrar registrar = new JmsListenerEndpointRegistrar(); @@ -324,8 +320,7 @@ private String getEndpointId(JmsListener jmsListener) { } } - @Nullable - private String resolve(String value) { + private @Nullable String resolve(String value) { return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); } @@ -338,8 +333,7 @@ private String resolve(String value) { */ private class MessageHandlerMethodFactoryAdapter implements MessageHandlerMethodFactory { - @Nullable - private MessageHandlerMethodFactory messageHandlerMethodFactory; + private @Nullable MessageHandlerMethodFactory messageHandlerMethodFactory; public void setMessageHandlerMethodFactory(MessageHandlerMethodFactory messageHandlerMethodFactory) { this.messageHandlerMethodFactory = messageHandlerMethodFactory; diff --git a/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java b/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java index 7aca3605b61c..3caf9f55bd77 100644 --- a/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotations and support classes for declarative JMS listener endpoints. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java index 32816c009275..0236120c7da8 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/AbstractJmsListenerContainerFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,12 +21,12 @@ import jakarta.jms.ExceptionListener; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jms.listener.AbstractMessageListenerContainer; import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; import org.springframework.util.ErrorHandler; /** @@ -42,53 +42,39 @@ public abstract class AbstractJmsListenerContainerFactory element. diff --git a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java index 0338478a0864..48c16ac13518 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJcaListenerContainerFactory.java @@ -17,12 +17,12 @@ package org.springframework.jms.config; import jakarta.resource.spi.ResourceAdapter; +import org.jspecify.annotations.Nullable; import org.springframework.jms.listener.endpoint.JmsActivationSpecConfig; import org.springframework.jms.listener.endpoint.JmsActivationSpecFactory; import org.springframework.jms.listener.endpoint.JmsMessageEndpointManager; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; /** * A {@link JmsListenerContainerFactory} implementation to build a @@ -34,20 +34,15 @@ public class DefaultJcaListenerContainerFactory extends JmsActivationSpecConfig implements JmsListenerContainerFactory { - @Nullable - private ResourceAdapter resourceAdapter; + private @Nullable ResourceAdapter resourceAdapter; - @Nullable - private JmsActivationSpecFactory activationSpecFactory; + private @Nullable JmsActivationSpecFactory activationSpecFactory; - @Nullable - private DestinationResolver destinationResolver; + private @Nullable DestinationResolver destinationResolver; - @Nullable - private Object transactionManager; + private @Nullable Object transactionManager; - @Nullable - private Integer phase; + private @Nullable Integer phase; /** diff --git a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java index f11d9ae7d89e..d3290a510499 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/DefaultJmsListenerContainerFactory.java @@ -18,8 +18,9 @@ import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; + import org.springframework.jms.listener.DefaultMessageListenerContainer; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.util.backoff.BackOff; @@ -36,32 +37,23 @@ public class DefaultJmsListenerContainerFactory extends AbstractJmsListenerContainerFactory { - @Nullable - private Executor taskExecutor; + private @Nullable Executor taskExecutor; - @Nullable - private PlatformTransactionManager transactionManager; + private @Nullable PlatformTransactionManager transactionManager; - @Nullable - private Integer cacheLevel; + private @Nullable Integer cacheLevel; - @Nullable - private String cacheLevelName; + private @Nullable String cacheLevelName; - @Nullable - private String concurrency; + private @Nullable String concurrency; - @Nullable - private Integer maxMessagesPerTask; + private @Nullable Integer maxMessagesPerTask; - @Nullable - private Long receiveTimeout; + private @Nullable Long receiveTimeout; - @Nullable - private Long recoveryInterval; + private @Nullable Long recoveryInterval; - @Nullable - private BackOff backOff; + private @Nullable BackOff backOff; /** diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java index 85d075415a9a..7eb7c6e19a58 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerContainerParser.java @@ -19,6 +19,7 @@ import java.util.Locale; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Element; import org.springframework.beans.MutablePropertyValues; @@ -26,7 +27,6 @@ import org.springframework.beans.factory.config.RuntimeBeanReference; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.beans.factory.xml.ParserContext; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -59,8 +59,7 @@ class JmsListenerContainerParser extends AbstractListenerContainerParser { @Override - @Nullable - protected RootBeanDefinition createContainerFactory(String factoryId, Element containerEle, ParserContext parserContext, + protected @Nullable RootBeanDefinition createContainerFactory(String factoryId, Element containerEle, ParserContext parserContext, PropertyValues commonContainerProperties, PropertyValues specificContainerProperties) { RootBeanDefinition factoryDef = new RootBeanDefinition(); diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java index faaf91197318..f815d76df04d 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistrar.java @@ -19,10 +19,11 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.support.DefaultMessageHandlerMethodFactory; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.util.Assert; @@ -37,20 +38,15 @@ */ public class JmsListenerEndpointRegistrar implements BeanFactoryAware, InitializingBean { - @Nullable - private JmsListenerEndpointRegistry endpointRegistry; + private @Nullable JmsListenerEndpointRegistry endpointRegistry; - @Nullable - private MessageHandlerMethodFactory messageHandlerMethodFactory; + private @Nullable MessageHandlerMethodFactory messageHandlerMethodFactory; - @Nullable - private JmsListenerContainerFactory containerFactory; + private @Nullable JmsListenerContainerFactory containerFactory; - @Nullable - private String containerFactoryBeanName; + private @Nullable String containerFactoryBeanName; - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; private final List endpointDescriptors = new ArrayList<>(); @@ -68,8 +64,7 @@ public void setEndpointRegistry(@Nullable JmsListenerEndpointRegistry endpointRe * Return the {@link JmsListenerEndpointRegistry} instance for this * registrar, may be {@code null}. */ - @Nullable - public JmsListenerEndpointRegistry getEndpointRegistry() { + public @Nullable JmsListenerEndpointRegistry getEndpointRegistry() { return this.endpointRegistry; } @@ -88,8 +83,7 @@ public void setMessageHandlerMethodFactory(@Nullable MessageHandlerMethodFactory /** * Return the custom {@link MessageHandlerMethodFactory} to use, if any. */ - @Nullable - public MessageHandlerMethodFactory getMessageHandlerMethodFactory() { + public @Nullable MessageHandlerMethodFactory getMessageHandlerMethodFactory() { return this.messageHandlerMethodFactory; } @@ -197,8 +191,7 @@ private static class JmsListenerEndpointDescriptor { public final JmsListenerEndpoint endpoint; - @Nullable - public final JmsListenerContainerFactory containerFactory; + public final @Nullable JmsListenerContainerFactory containerFactory; public JmsListenerEndpointDescriptor(JmsListenerEndpoint endpoint, @Nullable JmsListenerContainerFactory containerFactory) { diff --git a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java index b111fb1165b0..42ee66b1bb65 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/JmsListenerEndpointRegistry.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.DisposableBean; @@ -35,7 +36,6 @@ import org.springframework.context.SmartLifecycle; import org.springframework.context.event.ContextRefreshedEvent; import org.springframework.jms.listener.MessageListenerContainer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -69,8 +69,7 @@ public class JmsListenerEndpointRegistry implements DisposableBean, SmartLifecyc private int phase = DEFAULT_PHASE; - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; private boolean contextRefreshed; @@ -96,8 +95,7 @@ public void onApplicationEvent(ContextRefreshedEvent event) { * @see JmsListenerEndpoint#getId() * @see #getListenerContainerIds() */ - @Nullable - public MessageListenerContainer getListenerContainer(String id) { + public @Nullable MessageListenerContainer getListenerContainer(String id) { Assert.notNull(id, "Container identifier must not be null"); return this.listenerContainers.get(id); } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java index 3bedccadde5c..f6005558c4af 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/MethodJmsListenerEndpoint.java @@ -19,6 +19,8 @@ import java.lang.reflect.Method; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aop.support.AopUtils; import org.springframework.beans.factory.BeanFactory; @@ -31,7 +33,6 @@ import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.messaging.handler.annotation.support.MessageHandlerMethodFactory; import org.springframework.messaging.handler.invocation.InvocableHandlerMethod; @@ -49,20 +50,15 @@ */ public class MethodJmsListenerEndpoint extends AbstractJmsListenerEndpoint implements BeanFactoryAware { - @Nullable - private Object bean; + private @Nullable Object bean; - @Nullable - private Method method; + private @Nullable Method method; - @Nullable - private Method mostSpecificMethod; + private @Nullable Method mostSpecificMethod; - @Nullable - private MessageHandlerMethodFactory messageHandlerMethodFactory; + private @Nullable MessageHandlerMethodFactory messageHandlerMethodFactory; - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; /** @@ -72,8 +68,7 @@ public void setBean(@Nullable Object bean) { this.bean = bean; } - @Nullable - public Object getBean() { + public @Nullable Object getBean() { return this.bean; } @@ -84,8 +79,7 @@ public void setMethod(@Nullable Method method) { this.method = method; } - @Nullable - public Method getMethod() { + public @Nullable Method getMethod() { return this.method; } @@ -99,8 +93,7 @@ public void setMostSpecificMethod(@Nullable Method mostSpecificMethod) { this.mostSpecificMethod = mostSpecificMethod; } - @Nullable - public Method getMostSpecificMethod() { + public @Nullable Method getMostSpecificMethod() { if (this.mostSpecificMethod != null) { return this.mostSpecificMethod; } @@ -188,8 +181,7 @@ protected MessagingMessageListenerAdapter createMessageListenerInstance() { /** * Return the default response destination, if any. */ - @Nullable - protected String getDefaultResponseDestination() { + protected @Nullable String getDefaultResponseDestination() { Method specificMethod = getMostSpecificMethod(); if (specificMethod == null) { return null; @@ -206,8 +198,7 @@ protected String getDefaultResponseDestination() { return null; } - @Nullable - private SendTo getSendTo(Method specificMethod) { + private @Nullable SendTo getSendTo(Method specificMethod) { SendTo ann = AnnotatedElementUtils.findMergedAnnotation(specificMethod, SendTo.class); if (ann == null) { ann = AnnotatedElementUtils.findMergedAnnotation(specificMethod.getDeclaringClass(), SendTo.class); @@ -215,8 +206,7 @@ private SendTo getSendTo(Method specificMethod) { return ann; } - @Nullable - private String resolve(String value) { + private @Nullable String resolve(String value) { return (this.embeddedValueResolver != null ? this.embeddedValueResolver.resolveStringValue(value) : value); } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java b/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java index 5dfc42ad2883..6978e54659be 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/SimpleJmsListenerEndpoint.java @@ -17,9 +17,9 @@ package org.springframework.jms.config; import jakarta.jms.MessageListener; +import org.jspecify.annotations.Nullable; import org.springframework.jms.listener.MessageListenerContainer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -31,8 +31,7 @@ */ public class SimpleJmsListenerEndpoint extends AbstractJmsListenerEndpoint { - @Nullable - private MessageListener messageListener; + private @Nullable MessageListener messageListener; /** @@ -47,8 +46,7 @@ public void setMessageListener(@Nullable MessageListener messageListener) { * Return the {@link MessageListener} to invoke when a message matching * the endpoint is received. */ - @Nullable - public MessageListener getMessageListener() { + public @Nullable MessageListener getMessageListener() { return this.messageListener; } diff --git a/spring-jms/src/main/java/org/springframework/jms/config/package-info.java b/spring-jms/src/main/java/org/springframework/jms/config/package-info.java index 615ba9a71df1..391ad07a1923 100644 --- a/spring-jms/src/main/java/org/springframework/jms/config/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/config/package-info.java @@ -2,9 +2,7 @@ * Support package for declarative messaging configuration, * with Java configuration and XML schema support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java index 33c5708c4329..a64186666208 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageConsumer.java @@ -24,8 +24,7 @@ import jakarta.jms.QueueReceiver; import jakarta.jms.Topic; import jakarta.jms.TopicSubscriber; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * JMS MessageConsumer decorator that adapts all calls @@ -50,14 +49,12 @@ public String getMessageSelector() throws JMSException { } @Override - @Nullable - public Queue getQueue() throws JMSException { + public @Nullable Queue getQueue() throws JMSException { return (this.target instanceof QueueReceiver receiver ? receiver.getQueue() : null); } @Override - @Nullable - public Topic getTopic() throws JMSException { + public @Nullable Topic getTopic() throws JMSException { return (this.target instanceof TopicSubscriber subscriber ? subscriber.getTopic() : null); } diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java index 91f24ccd00a0..20cfc4b5fd72 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachedMessageProducer.java @@ -25,8 +25,7 @@ import jakarta.jms.QueueSender; import jakarta.jms.Topic; import jakarta.jms.TopicPublisher; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * JMS MessageProducer decorator that adapts calls to a shared MessageProducer @@ -39,14 +38,11 @@ class CachedMessageProducer implements MessageProducer, QueueSender, TopicPublis private final MessageProducer target; - @Nullable - private Boolean originalDisableMessageID; + private @Nullable Boolean originalDisableMessageID; - @Nullable - private Boolean originalDisableMessageTimestamp; + private @Nullable Boolean originalDisableMessageTimestamp; - @Nullable - private Long originalDeliveryDelay; + private @Nullable Long originalDeliveryDelay; private int deliveryMode; diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java index 6c24dc7828bb..9bca4e364b64 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/CachingConnectionFactory.java @@ -42,8 +42,8 @@ import jakarta.jms.TemporaryTopic; import jakarta.jms.Topic; import jakarta.jms.TopicSession; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -240,8 +240,7 @@ public void resetConnection() { * Checks for a cached Session for the given mode. */ @Override - @Nullable - protected Session getSession(Connection con, Integer mode) throws JMSException { + protected @Nullable Session getSession(Connection con, Integer mode) throws JMSException { if (!this.active) { return null; } @@ -312,8 +311,7 @@ public CachedSessionInvocationHandler(Session target, Deque sessionList } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); if (methodName.equals("equals")) { // Only consider equal when proxies are identical. @@ -538,8 +536,7 @@ private static class DestinationCacheKey implements Comparable C getConnection(Class connectionType) { + public @Nullable C getConnection(Class connectionType) { return CollectionUtils.findValueOfType(this.connections, connectionType); } @@ -194,8 +191,7 @@ public C getConnection(Class connectionType) { *

    In contrast to {@link #getSession()}, this must not lazily initialize * a new Session, not even in {@link JmsResourceHolder} subclasses. */ - @Nullable - Session getOriginalSession() { + @Nullable Session getOriginalSession() { return this.sessions.peek(); } @@ -203,8 +199,7 @@ Session getOriginalSession() { * Return this resource holder's default Session, * or {@code null} if none. */ - @Nullable - public Session getSession() { + public @Nullable Session getSession() { return this.sessions.peek(); } @@ -212,8 +207,7 @@ public Session getSession() { * Return this resource holder's Session of the given type, * or {@code null} if none. */ - @Nullable - public S getSession(Class sessionType) { + public @Nullable S getSession(Class sessionType) { return getSession(sessionType, null); } @@ -221,9 +215,7 @@ public S getSession(Class sessionType) { * Return this resource holder's Session of the given type * for the given connection, or {@code null} if none. */ - @Nullable - @SuppressWarnings("NullAway") - public S getSession(Class sessionType, @Nullable Connection connection) { + public @Nullable S getSession(Class sessionType, @Nullable Connection connection) { Deque sessions = (connection != null ? this.sessionsPerConnection.get(connection) : this.sessions); return CollectionUtils.findValueOfType(sessions, sessionType); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java b/spring-jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java index 3ffd27e867d7..99a6820e57ae 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/JmsTransactionManager.java @@ -21,9 +21,9 @@ import jakarta.jms.JMSException; import jakarta.jms.Session; import jakarta.jms.TransactionRolledBackException; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.InvalidIsolationLevelException; import org.springframework.transaction.TransactionDefinition; @@ -93,8 +93,7 @@ public class JmsTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean { - @Nullable - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; private boolean lazyResourceRetrieval = false; @@ -144,8 +143,7 @@ public void setConnectionFactory(@Nullable ConnectionFactory cf) { /** * Return the JMS ConnectionFactory that this instance should manage transactions for. */ - @Nullable - public ConnectionFactory getConnectionFactory() { + public @Nullable ConnectionFactory getConnectionFactory() { return this.connectionFactory; } @@ -357,36 +355,31 @@ public LazyJmsResourceHolder(@Nullable ConnectionFactory connectionFactory) { } @Override - @Nullable - public Connection getConnection() { + public @Nullable Connection getConnection() { initializeConnection(); return super.getConnection(); } @Override - @Nullable - public C getConnection(Class connectionType) { + public @Nullable C getConnection(Class connectionType) { initializeConnection(); return super.getConnection(connectionType); } @Override - @Nullable - public Session getSession() { + public @Nullable Session getSession() { initializeSession(); return super.getSession(); } @Override - @Nullable - public S getSession(Class sessionType) { + public @Nullable S getSession(Class sessionType) { initializeSession(); return super.getSession(sessionType); } @Override - @Nullable - public S getSession(Class sessionType, @Nullable Connection connection) { + public @Nullable S getSession(Class sessionType, @Nullable Connection connection) { initializeSession(); return super.getSession(sessionType, connection); } @@ -428,8 +421,7 @@ private void initializeSession() { */ private static class JmsTransactionObject implements SmartTransactionObject { - @Nullable - private JmsResourceHolder resourceHolder; + private @Nullable JmsResourceHolder resourceHolder; public void setResourceHolder(@Nullable JmsResourceHolder resourceHolder) { this.resourceHolder = resourceHolder; diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java index 186fcaa3baab..d64ad0414b47 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/SingleConnectionFactory.java @@ -39,11 +39,11 @@ import jakarta.jms.TopicConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.Lifecycle; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -92,28 +92,22 @@ public class SingleConnectionFactory implements ConnectionFactory, QueueConnecti protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private ConnectionFactory targetConnectionFactory; + private @Nullable ConnectionFactory targetConnectionFactory; - @Nullable - private String clientId; + private @Nullable String clientId; - @Nullable - private ExceptionListener exceptionListener; + private @Nullable ExceptionListener exceptionListener; private boolean reconnectOnException = false; /** The target Connection. */ - @Nullable - private Connection connection; + private @Nullable Connection connection; /** A hint whether to create a queue or topic connection. */ - @Nullable - private Boolean pubSubMode; + private @Nullable Boolean pubSubMode; /** An internal aggregator allowing for per-connection ExceptionListeners. */ - @Nullable - private AggregatedExceptionListener aggregatedExceptionListener; + private @Nullable AggregatedExceptionListener aggregatedExceptionListener; /** Whether the shared Connection has been started. */ private int startedCount = 0; @@ -161,8 +155,7 @@ public void setTargetConnectionFactory(@Nullable ConnectionFactory targetConnect * Return the target ConnectionFactory which will be used to lazily * create a single Connection, if any. */ - @Nullable - public ConnectionFactory getTargetConnectionFactory() { + public @Nullable ConnectionFactory getTargetConnectionFactory() { return this.targetConnectionFactory; } @@ -183,8 +176,7 @@ public void setClientId(@Nullable String clientId) { * Return a JMS client ID for the single Connection created and exposed * by this ConnectionFactory, if any. */ - @Nullable - protected String getClientId() { + protected @Nullable String getClientId() { return this.clientId; } @@ -201,8 +193,7 @@ public void setExceptionListener(@Nullable ExceptionListener exceptionListener) * Return the JMS ExceptionListener implementation that should be registered * with the single Connection created by this factory, if any. */ - @Nullable - protected ExceptionListener getExceptionListener() { + protected @Nullable ExceptionListener getExceptionListener() { return this.exceptionListener; } @@ -332,7 +323,7 @@ private ConnectionFactory obtainTargetConnectionFactory() { * @throws jakarta.jms.JMSException if thrown by JMS API methods * @see #initConnection() */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected Connection getConnection() throws JMSException { this.connectionLock.lock(); try { @@ -519,8 +510,7 @@ else if (getExceptionListener() != null || isReconnectOnException()) { * creation of a raw standard Session * @throws JMSException if thrown by the JMS API */ - @Nullable - protected Session getSession(Connection con, Integer mode) throws JMSException { + protected @Nullable Session getSession(Connection con, Integer mode) throws JMSException { return null; } @@ -616,14 +606,12 @@ protected Connection getSharedConnectionProxy(Connection target) { */ private class SharedConnectionInvocationHandler implements InvocationHandler { - @Nullable - private ExceptionListener localExceptionListener; + private @Nullable ExceptionListener localExceptionListener; private boolean locallyStarted = false; @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) { case "equals" -> { Object other = args[0]; diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java b/spring-jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java index e5ffb4b0673a..dab2aef3ac37 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/TransactionAwareConnectionFactoryProxy.java @@ -35,8 +35,8 @@ import jakarta.jms.TopicConnectionFactory; import jakarta.jms.TopicSession; import jakarta.jms.TransactionInProgressException; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -86,8 +86,7 @@ public class TransactionAwareConnectionFactoryProxy implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory { - @Nullable - private ConnectionFactory targetConnectionFactory; + private @Nullable ConnectionFactory targetConnectionFactory; private boolean synchedLocalTransactionAllowed = false; @@ -327,8 +326,7 @@ public CloseSuppressingSessionInvocationHandler(Session target) { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on SessionProxy interface coming in... return switch (method.getName()) { 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 5d5868e8b9cd..3556fd3e6817 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. @@ -24,10 +24,10 @@ import jakarta.jms.QueueConnectionFactory; import jakarta.jms.TopicConnection; import jakarta.jms.TopicConnectionFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -36,7 +36,7 @@ * 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 @@ -81,14 +81,11 @@ public class UserCredentialsConnectionFactoryAdapter implements ConnectionFactory, QueueConnectionFactory, TopicConnectionFactory, InitializingBean { - @Nullable - private ConnectionFactory targetConnectionFactory; + private @Nullable ConnectionFactory targetConnectionFactory; - @Nullable - private String username; + private @Nullable String username; - @Nullable - private String password; + private @Nullable String password; private final ThreadLocal threadBoundCredentials = new NamedThreadLocal<>("Current JMS user credentials"); diff --git a/spring-jms/src/main/java/org/springframework/jms/connection/package-info.java b/spring-jms/src/main/java/org/springframework/jms/connection/package-info.java index f075ee2297cc..3bc4d6ccbc4a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/connection/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/connection/package-info.java @@ -2,9 +2,7 @@ * Provides a PlatformTransactionManager implementation for a single * JMS ConnectionFactory, and a SingleConnectionFactory adapter. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.connection; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/core/BrowserCallback.java b/spring-jms/src/main/java/org/springframework/jms/core/BrowserCallback.java index 1a076bad5012..303267cb3a59 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/BrowserCallback.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/BrowserCallback.java @@ -19,8 +19,7 @@ import jakarta.jms.JMSException; import jakarta.jms.QueueBrowser; import jakarta.jms.Session; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Callback for browsing the messages in a JMS queue. @@ -47,7 +46,6 @@ public interface BrowserCallback { * (or {@code null} if none) * @throws jakarta.jms.JMSException if thrown by JMS API methods */ - @Nullable - T doInJms(Session session, QueueBrowser browser) throws JMSException; + @Nullable T doInJms(Session session, QueueBrowser browser) throws JMSException; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessageOperations.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessageOperations.java index ebb821da62cd..921cab42ebff 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessageOperations.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessageOperations.java @@ -19,8 +19,8 @@ import java.util.Map; import jakarta.jms.Destination; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.MessagePostProcessor; @@ -102,8 +102,7 @@ void convertAndSend(String destinationName, Object payload, @Nullable Map receive(String destinationName) throws MessagingException; + @Nullable Message receive(String destinationName) throws MessagingException; /** * Receive a message from the given destination and convert its payload to the @@ -113,8 +112,7 @@ void convertAndSend(String destinationName, Object payload, @Nullable Map T receiveAndConvert(String destinationName, Class targetClass) throws MessagingException; + @Nullable T receiveAndConvert(String destinationName, Class targetClass) throws MessagingException; /** * Send a request message and receive the reply from the given destination. @@ -123,8 +121,7 @@ void convertAndSend(String destinationName, Object payload, @Nullable Map sendAndReceive(String destinationName, Message requestMessage) throws MessagingException; + @Nullable Message sendAndReceive(String destinationName, Message requestMessage) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -137,8 +134,7 @@ void convertAndSend(String destinationName, Object payload, @Nullable Map T convertSendAndReceive(String destinationName, Object request, Class targetClass) throws MessagingException; + @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -152,8 +148,7 @@ void convertAndSend(String destinationName, Object payload, @Nullable Map T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass) + @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass) throws MessagingException; /** @@ -169,8 +164,7 @@ T convertSendAndReceive(String destinationName, Object request, @Nullable Ma * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(String destinationName, Object request, Class targetClass, + @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass, MessagePostProcessor requestPostProcessor) throws MessagingException; /** @@ -186,8 +180,7 @@ T convertSendAndReceive(String destinationName, Object request, Class tar * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(String destinationName, Object request, Map headers, + @Nullable T convertSendAndReceive(String destinationName, Object request, Map headers, Class targetClass, MessagePostProcessor requestPostProcessor) throws MessagingException; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java index 5c0aeb27b959..4b87ce0fb2b2 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsMessagingTemplate.java @@ -22,6 +22,7 @@ import jakarta.jms.Destination; import jakarta.jms.JMSException; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.jms.InvalidDestinationException; @@ -29,7 +30,6 @@ import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.MessagingMessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.converter.MessageConversionException; @@ -48,15 +48,13 @@ public class JmsMessagingTemplate extends AbstractMessagingTemplate implements JmsMessageOperations, InitializingBean { - @Nullable - private JmsTemplate jmsTemplate; + private @Nullable JmsTemplate jmsTemplate; private MessageConverter jmsMessageConverter = new MessagingMessageConverter(); private boolean converterSet; - @Nullable - private String defaultDestinationName; + private @Nullable String defaultDestinationName; /** @@ -101,8 +99,7 @@ public void setConnectionFactory(ConnectionFactory connectionFactory) { * Return the ConnectionFactory that the underlying {@link JmsTemplate} uses. * @since 4.1.2 */ - @Nullable - public ConnectionFactory getConnectionFactory() { + public @Nullable ConnectionFactory getConnectionFactory() { return (this.jmsTemplate != null ? this.jmsTemplate.getConnectionFactory() : null); } @@ -116,8 +113,7 @@ public void setJmsTemplate(@Nullable JmsTemplate jmsTemplate) { /** * Return the configured {@link JmsTemplate}. */ - @Nullable - public JmsTemplate getJmsTemplate() { + public @Nullable JmsTemplate getJmsTemplate() { return this.jmsTemplate; } @@ -158,8 +154,7 @@ public void setDefaultDestinationName(@Nullable String defaultDestinationName) { /** * Return the configured default destination name. */ - @Nullable - public String getDefaultDestinationName() { + public @Nullable String getDefaultDestinationName() { return this.defaultDestinationName; } @@ -233,8 +228,7 @@ public void convertAndSend(String destinationName, Object payload, @Nullable Map } @Override - @Nullable - public Message receive() { + public @Nullable Message receive() { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return receive(defaultDestination); @@ -245,8 +239,7 @@ public Message receive() { } @Override - @Nullable - public T receiveAndConvert(Class targetClass) { + public @Nullable T receiveAndConvert(Class targetClass) { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return receiveAndConvert(defaultDestination, targetClass); @@ -257,14 +250,12 @@ public T receiveAndConvert(Class targetClass) { } @Override - @Nullable - public Message receive(String destinationName) throws MessagingException { + public @Nullable Message receive(String destinationName) throws MessagingException { return doReceive(destinationName); } @Override - @Nullable - public T receiveAndConvert(String destinationName, Class targetClass) throws MessagingException { + public @Nullable T receiveAndConvert(String destinationName, Class targetClass) throws MessagingException { Message message = doReceive(destinationName); if (message != null) { return doConvert(message, targetClass); @@ -275,8 +266,7 @@ public T receiveAndConvert(String destinationName, Class targetClass) thr } @Override - @Nullable - public Message sendAndReceive(Message requestMessage) { + public @Nullable Message sendAndReceive(Message requestMessage) { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return sendAndReceive(defaultDestination, requestMessage); @@ -287,36 +277,31 @@ public Message sendAndReceive(Message requestMessage) { } @Override - @Nullable - public Message sendAndReceive(String destinationName, Message requestMessage) throws MessagingException { + public @Nullable Message sendAndReceive(String destinationName, Message requestMessage) throws MessagingException { return doSendAndReceive(destinationName, requestMessage); } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, Class targetClass) + public @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass) throws MessagingException { return convertSendAndReceive(destinationName, request, null, targetClass); } @Override - @Nullable - public T convertSendAndReceive(Object request, Class targetClass) { + public @Nullable T convertSendAndReceive(Object request, Class targetClass) { return convertSendAndReceive(request, targetClass, null); } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, + public @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass) throws MessagingException { return convertSendAndReceive(destinationName, request, headers, targetClass, null); } @Override - @Nullable - public T convertSendAndReceive(Object request, Class targetClass, @Nullable MessagePostProcessor postProcessor) { + public @Nullable T convertSendAndReceive(Object request, Class targetClass, @Nullable MessagePostProcessor postProcessor) { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return convertSendAndReceive(defaultDestination, request, targetClass, postProcessor); @@ -327,8 +312,7 @@ public T convertSendAndReceive(Object request, Class targetClass, @Nullab } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, Class targetClass, + public @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException { return convertSendAndReceive(destinationName, request, null, targetClass, requestPostProcessor); @@ -336,8 +320,7 @@ public T convertSendAndReceive(String destinationName, Object request, Class @SuppressWarnings("unchecked") @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, + public @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor postProcessor) { Message requestMessage = doConvert(request, headers, postProcessor); @@ -365,8 +348,7 @@ protected void doSend(String destinationName, Message message) { } @Override - @Nullable - protected Message doReceive(Destination destination) { + protected @Nullable Message doReceive(Destination destination) { try { jakarta.jms.Message jmsMessage = obtainJmsTemplate().receive(destination); return convertJmsMessage(jmsMessage); @@ -376,8 +358,7 @@ protected Message doReceive(Destination destination) { } } - @Nullable - protected Message doReceive(String destinationName) { + protected @Nullable Message doReceive(String destinationName) { try { jakarta.jms.Message jmsMessage = obtainJmsTemplate().receive(destinationName); return convertJmsMessage(jmsMessage); @@ -388,8 +369,7 @@ protected Message doReceive(String destinationName) { } @Override - @Nullable - protected Message doSendAndReceive(Destination destination, Message requestMessage) { + protected @Nullable Message doSendAndReceive(Destination destination, Message requestMessage) { try { jakarta.jms.Message jmsMessage = obtainJmsTemplate().sendAndReceive( destination, createMessageCreator(requestMessage)); @@ -400,8 +380,7 @@ protected Message doSendAndReceive(Destination destination, Message reques } } - @Nullable - protected Message doSendAndReceive(String destinationName, Message requestMessage) { + protected @Nullable Message doSendAndReceive(String destinationName, Message requestMessage) { try { jakarta.jms.Message jmsMessage = obtainJmsTemplate().sendAndReceive( destinationName, createMessageCreator(requestMessage)); @@ -425,8 +404,7 @@ protected String getRequiredDefaultDestinationName() { return name; } - @Nullable - protected Message convertJmsMessage(@Nullable jakarta.jms.Message message) { + protected @Nullable Message convertJmsMessage(jakarta.jms.@Nullable Message message) { if (message == null) { return null; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java index 0aa7888819a1..2245d596522e 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsOperations.java @@ -19,9 +19,9 @@ import jakarta.jms.Destination; import jakarta.jms.Message; import jakarta.jms.Queue; +import org.jspecify.annotations.Nullable; import org.springframework.jms.JmsException; -import org.springframework.lang.Nullable; /** * Specifies a basic set of JMS operations. @@ -54,8 +54,7 @@ public interface JmsOperations { * @return the result object from working with the session * @throws JmsException if there is any problem */ - @Nullable - T execute(SessionCallback action) throws JmsException; + @Nullable T execute(SessionCallback action) throws JmsException; /** * Send messages to the default JMS destination (or one specified @@ -65,8 +64,7 @@ public interface JmsOperations { * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T execute(ProducerCallback action) throws JmsException; + @Nullable T execute(ProducerCallback action) throws JmsException; /** * Send messages to a JMS destination. The callback gives access to the JMS Session @@ -76,8 +74,7 @@ public interface JmsOperations { * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T execute(Destination destination, ProducerCallback action) throws JmsException; + @Nullable T execute(Destination destination, ProducerCallback action) throws JmsException; /** * Send messages to a JMS destination. The callback gives access to the JMS Session @@ -88,8 +85,7 @@ public interface JmsOperations { * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T execute(String destinationName, ProducerCallback action) throws JmsException; + @Nullable T execute(String destinationName, ProducerCallback action) throws JmsException; //--------------------------------------------------------------------------------------- @@ -207,8 +203,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message received by the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Message receive() throws JmsException; + @Nullable Message receive() throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -219,8 +214,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message received by the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Message receive(Destination destination) throws JmsException; + @Nullable Message receive(Destination destination) throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -232,8 +226,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message received by the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Message receive(String destinationName) throws JmsException; + @Nullable Message receive(String destinationName) throws JmsException; /** * Receive a message synchronously from the default destination, but only @@ -246,8 +239,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message received by the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Message receiveSelected(String messageSelector) throws JmsException; + @Nullable Message receiveSelected(String messageSelector) throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -260,8 +252,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message received by the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Message receiveSelected(Destination destination, String messageSelector) throws JmsException; + @Nullable Message receiveSelected(Destination destination, String messageSelector) throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -275,8 +266,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message received by the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Message receiveSelected(String destinationName, String messageSelector) throws JmsException; + @Nullable Message receiveSelected(String destinationName, String messageSelector) throws JmsException; //--------------------------------------------------------------------------------------- @@ -293,8 +283,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Object receiveAndConvert() throws JmsException; + @Nullable Object receiveAndConvert() throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -306,8 +295,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Object receiveAndConvert(Destination destination) throws JmsException; + @Nullable Object receiveAndConvert(Destination destination) throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -320,8 +308,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Object receiveAndConvert(String destinationName) throws JmsException; + @Nullable Object receiveAndConvert(String destinationName) throws JmsException; /** * Receive a message synchronously from the default destination, but only @@ -335,8 +322,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Object receiveSelectedAndConvert(String messageSelector) throws JmsException; + @Nullable Object receiveSelectedAndConvert(String messageSelector) throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -350,8 +336,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Object receiveSelectedAndConvert(Destination destination, String messageSelector) throws JmsException; + @Nullable Object receiveSelectedAndConvert(Destination destination, String messageSelector) throws JmsException; /** * Receive a message synchronously from the specified destination, but only @@ -366,8 +351,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the message produced for the consumer, or {@code null} if the timeout expires * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - Object receiveSelectedAndConvert(String destinationName, String messageSelector) throws JmsException; + @Nullable Object receiveSelectedAndConvert(String destinationName, String messageSelector) throws JmsException; //--------------------------------------------------------------------------------------- @@ -386,8 +370,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @throws JmsException checked JMSException converted to unchecked * @since 4.1 */ - @Nullable - Message sendAndReceive(MessageCreator messageCreator) throws JmsException; + @Nullable Message sendAndReceive(MessageCreator messageCreator) throws JmsException; /** * Send a message and receive the reply from the specified destination. The @@ -401,8 +384,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @throws JmsException checked JMSException converted to unchecked * @since 4.1 */ - @Nullable - Message sendAndReceive(Destination destination, MessageCreator messageCreator) throws JmsException; + @Nullable Message sendAndReceive(Destination destination, MessageCreator messageCreator) throws JmsException; /** * Send a message and receive the reply from the specified destination. The @@ -417,8 +399,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @throws JmsException checked JMSException converted to unchecked * @since 4.1 */ - @Nullable - Message sendAndReceive(String destinationName, MessageCreator messageCreator) throws JmsException; + @Nullable Message sendAndReceive(String destinationName, MessageCreator messageCreator) throws JmsException; //--------------------------------------------------------------------------------------- @@ -432,8 +413,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T browse(BrowserCallback action) throws JmsException; + @Nullable T browse(BrowserCallback action) throws JmsException; /** * Browse messages in a JMS queue. The callback gives access to the JMS Session @@ -443,8 +423,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T browse(Queue queue, BrowserCallback action) throws JmsException; + @Nullable T browse(Queue queue, BrowserCallback action) throws JmsException; /** * Browse messages in a JMS queue. The callback gives access to the JMS Session @@ -455,8 +434,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T browse(String queueName, BrowserCallback action) throws JmsException; + @Nullable T browse(String queueName, BrowserCallback action) throws JmsException; /** * Browse selected messages in a JMS queue. The callback gives access to the JMS @@ -467,8 +445,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T browseSelected(String messageSelector, BrowserCallback action) throws JmsException; + @Nullable T browseSelected(String messageSelector, BrowserCallback action) throws JmsException; /** * Browse selected messages in a JMS queue. The callback gives access to the JMS @@ -480,8 +457,7 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T browseSelected(Queue queue, String messageSelector, BrowserCallback action) throws JmsException; + @Nullable T browseSelected(Queue queue, String messageSelector, BrowserCallback action) throws JmsException; /** * Browse selected messages in a JMS queue. The callback gives access to the JMS @@ -494,7 +470,6 @@ void convertAndSend(String destinationName, Object message, MessagePostProcessor * @return the result object from working with the session * @throws JmsException checked JMSException converted to unchecked */ - @Nullable - T browseSelected(String queueName, String messageSelector, BrowserCallback action) throws JmsException; + @Nullable T browseSelected(String queueName, String messageSelector, BrowserCallback action) throws JmsException; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java b/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java index ecae541c4e31..3fef51127710 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/JmsTemplate.java @@ -30,6 +30,7 @@ import jakarta.jms.QueueBrowser; import jakarta.jms.Session; import jakarta.jms.TemporaryQueue; +import org.jspecify.annotations.Nullable; import org.springframework.jms.JmsException; import org.springframework.jms.connection.ConnectionFactoryUtils; @@ -39,7 +40,6 @@ import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; import org.springframework.jms.support.destination.JmsDestinationAccessor; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -99,11 +99,9 @@ public class JmsTemplate extends JmsDestinationAccessor implements JmsOperations private final JmsTemplateResourceFactory transactionalResourceFactory = new JmsTemplateResourceFactory(); - @Nullable - private Object defaultDestination; + private @Nullable Object defaultDestination; - @Nullable - private MessageConverter messageConverter; + private @Nullable MessageConverter messageConverter; private boolean messageIdEnabled = true; @@ -125,8 +123,7 @@ public class JmsTemplate extends JmsDestinationAccessor implements JmsOperations private long timeToLive = Message.DEFAULT_TIME_TO_LIVE; - @Nullable - private ObservationRegistry observationRegistry; + private @Nullable ObservationRegistry observationRegistry; /** @@ -181,13 +178,11 @@ public void setDefaultDestination(@Nullable Destination destination) { * Return the destination to be used on send/receive operations that do not * have a destination parameter. */ - @Nullable - public Destination getDefaultDestination() { + public @Nullable Destination getDefaultDestination() { return (this.defaultDestination instanceof Destination dest ? dest : null); } - @Nullable - private Queue getDefaultQueue() { + private @Nullable Queue getDefaultQueue() { Destination defaultDestination = getDefaultDestination(); if (defaultDestination == null) { return null; @@ -218,8 +213,7 @@ public void setDefaultDestinationName(@Nullable String destinationName) { * Return the destination name to be used on send/receive operations that * do not have a destination parameter. */ - @Nullable - public String getDefaultDestinationName() { + public @Nullable String getDefaultDestinationName() { return (this.defaultDestination instanceof String name ? name : null); } @@ -249,8 +243,7 @@ public void setMessageConverter(@Nullable MessageConverter messageConverter) { /** * Return the message converter for this template. */ - @Nullable - public MessageConverter getMessageConverter() { + public @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -485,8 +478,7 @@ public void setObservationRegistry(ObservationRegistry observationRegistry) { //--------------------------------------------------------------------------------------- @Override - @Nullable - public T execute(SessionCallback action) throws JmsException { + public @Nullable T execute(SessionCallback action) throws JmsException { return execute(action, false); } @@ -505,8 +497,7 @@ public T execute(SessionCallback action) throws JmsException { * @see #receive */ @SuppressWarnings("resource") - @Nullable - public T execute(SessionCallback action, boolean startConnection) throws JmsException { + public @Nullable T execute(SessionCallback action, boolean startConnection) throws JmsException { Assert.notNull(action, "Callback object must not be null"); Connection conToClose = null; Session sessionToClose = null; @@ -539,8 +530,7 @@ public T execute(SessionCallback action, boolean startConnection) throws } @Override - @Nullable - public T execute(ProducerCallback action) throws JmsException { + public @Nullable T execute(ProducerCallback action) throws JmsException { String defaultDestinationName = getDefaultDestinationName(); if (defaultDestinationName != null) { return execute(defaultDestinationName, action); @@ -551,8 +541,7 @@ public T execute(ProducerCallback action) throws JmsException { } @Override - @Nullable - public T execute(final @Nullable Destination destination, final ProducerCallback action) throws JmsException { + public @Nullable T execute(final @Nullable Destination destination, final ProducerCallback action) throws JmsException { Assert.notNull(action, "Callback object must not be null"); return execute(session -> { MessageProducer producer = createProducer(session, destination); @@ -566,8 +555,7 @@ public T execute(final @Nullable Destination destination, final ProducerCall } @Override - @Nullable - public T execute(final String destinationName, final ProducerCallback action) throws JmsException { + public @Nullable T execute(final String destinationName, final ProducerCallback action) throws JmsException { Assert.notNull(action, "Callback object must not be null"); return execute(session -> { Destination destination = resolveDestinationName(session, destinationName); @@ -726,8 +714,7 @@ public void convertAndSend( //--------------------------------------------------------------------------------------- @Override - @Nullable - public Message receive() throws JmsException { + public @Nullable Message receive() throws JmsException { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return receive(defaultDestination); @@ -738,20 +725,17 @@ public Message receive() throws JmsException { } @Override - @Nullable - public Message receive(Destination destination) throws JmsException { + public @Nullable Message receive(Destination destination) throws JmsException { return receiveSelected(destination, null); } @Override - @Nullable - public Message receive(String destinationName) throws JmsException { + public @Nullable Message receive(String destinationName) throws JmsException { return receiveSelected(destinationName, null); } @Override - @Nullable - public Message receiveSelected(String messageSelector) throws JmsException { + public @Nullable Message receiveSelected(String messageSelector) throws JmsException { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return receiveSelected(defaultDestination, messageSelector); @@ -762,14 +746,12 @@ public Message receiveSelected(String messageSelector) throws JmsException { } @Override - @Nullable - public Message receiveSelected(final Destination destination, @Nullable final String messageSelector) throws JmsException { + public @Nullable Message receiveSelected(final Destination destination, final @Nullable String messageSelector) throws JmsException { return execute(session -> doReceive(session, destination, messageSelector), true); } @Override - @Nullable - public Message receiveSelected(final String destinationName, @Nullable final String messageSelector) throws JmsException { + public @Nullable Message receiveSelected(final String destinationName, final @Nullable String messageSelector) throws JmsException { return execute(session -> { Destination destination = resolveDestinationName(session, destinationName); return doReceive(session, destination, messageSelector); @@ -784,8 +766,7 @@ public Message receiveSelected(final String destinationName, @Nullable final Str * @return the JMS Message received, or {@code null} if none * @throws JMSException if thrown by JMS API methods */ - @Nullable - protected Message doReceive(Session session, Destination destination, @Nullable String messageSelector) + protected @Nullable Message doReceive(Session session, Destination destination, @Nullable String messageSelector) throws JMSException { return doReceive(session, createConsumer(session, destination, messageSelector)); @@ -798,8 +779,7 @@ protected Message doReceive(Session session, Destination destination, @Nullable * @return the JMS Message received, or {@code null} if none * @throws JMSException if thrown by JMS API methods */ - @Nullable - protected Message doReceive(Session session, MessageConsumer consumer) throws JMSException { + protected @Nullable Message doReceive(Session session, MessageConsumer consumer) throws JMSException { try { // Use transaction timeout (if available). long timeout = getReceiveTimeout(); @@ -838,38 +818,32 @@ else if (isClientAcknowledge(session)) { //--------------------------------------------------------------------------------------- @Override - @Nullable - public Object receiveAndConvert() throws JmsException { + public @Nullable Object receiveAndConvert() throws JmsException { return doConvertFromMessage(receive()); } @Override - @Nullable - public Object receiveAndConvert(Destination destination) throws JmsException { + public @Nullable Object receiveAndConvert(Destination destination) throws JmsException { return doConvertFromMessage(receive(destination)); } @Override - @Nullable - public Object receiveAndConvert(String destinationName) throws JmsException { + public @Nullable Object receiveAndConvert(String destinationName) throws JmsException { return doConvertFromMessage(receive(destinationName)); } @Override - @Nullable - public Object receiveSelectedAndConvert(String messageSelector) throws JmsException { + public @Nullable Object receiveSelectedAndConvert(String messageSelector) throws JmsException { return doConvertFromMessage(receiveSelected(messageSelector)); } @Override - @Nullable - public Object receiveSelectedAndConvert(Destination destination, String messageSelector) throws JmsException { + public @Nullable Object receiveSelectedAndConvert(Destination destination, String messageSelector) throws JmsException { return doConvertFromMessage(receiveSelected(destination, messageSelector)); } @Override - @Nullable - public Object receiveSelectedAndConvert(String destinationName, String messageSelector) throws JmsException { + public @Nullable Object receiveSelectedAndConvert(String destinationName, String messageSelector) throws JmsException { return doConvertFromMessage(receiveSelected(destinationName, messageSelector)); } @@ -878,8 +852,7 @@ public Object receiveSelectedAndConvert(String destinationName, String messageSe * @param message the JMS Message to convert (can be {@code null}) * @return the content of the message, or {@code null} if none */ - @Nullable - protected Object doConvertFromMessage(@Nullable Message message) { + protected @Nullable Object doConvertFromMessage(@Nullable Message message) { if (message != null) { try { return getRequiredMessageConverter().fromMessage(message); @@ -897,8 +870,7 @@ protected Object doConvertFromMessage(@Nullable Message message) { //--------------------------------------------------------------------------------------- @Override - @Nullable - public Message sendAndReceive(MessageCreator messageCreator) throws JmsException { + public @Nullable Message sendAndReceive(MessageCreator messageCreator) throws JmsException { Destination defaultDestination = getDefaultDestination(); if (defaultDestination != null) { return sendAndReceive(defaultDestination, messageCreator); @@ -909,14 +881,12 @@ public Message sendAndReceive(MessageCreator messageCreator) throws JmsException } @Override - @Nullable - public Message sendAndReceive(final Destination destination, final MessageCreator messageCreator) throws JmsException { + public @Nullable Message sendAndReceive(final Destination destination, final MessageCreator messageCreator) throws JmsException { return executeLocal(session -> doSendAndReceive(session, destination, messageCreator), true); } @Override - @Nullable - public Message sendAndReceive(final String destinationName, final MessageCreator messageCreator) throws JmsException { + public @Nullable Message sendAndReceive(final String destinationName, final MessageCreator messageCreator) throws JmsException { return executeLocal(session -> { Destination destination = resolveDestinationName(session, destinationName); return doSendAndReceive(session, destination, messageCreator); @@ -929,8 +899,7 @@ public Message sendAndReceive(final String destinationName, final MessageCreator *

    Return the response message or {@code null} if no message has * @throws JMSException if thrown by JMS API methods */ - @Nullable - protected Message doSendAndReceive(Session session, Destination destination, MessageCreator messageCreator) + protected @Nullable Message doSendAndReceive(Session session, Destination destination, MessageCreator messageCreator) throws JMSException { Assert.notNull(messageCreator, "MessageCreator must not be null"); @@ -963,8 +932,7 @@ protected Message doSendAndReceive(Session session, Destination destination, Mes * creates a non-transactional {@link Session}. The given {@link SessionCallback} * does not participate in an existing transaction. */ - @Nullable - private T executeLocal(SessionCallback action, boolean startConnection) throws JmsException { + private @Nullable T executeLocal(SessionCallback action, boolean startConnection) throws JmsException { Assert.notNull(action, "Callback object must not be null"); Connection con = null; Session session = null; @@ -997,8 +965,7 @@ private T executeLocal(SessionCallback action, boolean startConnection) t //--------------------------------------------------------------------------------------- @Override - @Nullable - public T browse(BrowserCallback action) throws JmsException { + public @Nullable T browse(BrowserCallback action) throws JmsException { Queue defaultQueue = getDefaultQueue(); if (defaultQueue != null) { return browse(defaultQueue, action); @@ -1009,20 +976,17 @@ public T browse(BrowserCallback action) throws JmsException { } @Override - @Nullable - public T browse(Queue queue, BrowserCallback action) throws JmsException { + public @Nullable T browse(Queue queue, BrowserCallback action) throws JmsException { return browseSelected(queue, null, action); } @Override - @Nullable - public T browse(String queueName, BrowserCallback action) throws JmsException { + public @Nullable T browse(String queueName, BrowserCallback action) throws JmsException { return browseSelected(queueName, null, action); } @Override - @Nullable - public T browseSelected(String messageSelector, BrowserCallback action) throws JmsException { + public @Nullable T browseSelected(String messageSelector, BrowserCallback action) throws JmsException { Queue defaultQueue = getDefaultQueue(); if (defaultQueue != null) { return browseSelected(defaultQueue, messageSelector, action); @@ -1033,8 +997,7 @@ public T browseSelected(String messageSelector, BrowserCallback action) t } @Override - @Nullable - public T browseSelected(final Queue queue, @Nullable final String messageSelector, final BrowserCallback action) + public @Nullable T browseSelected(final Queue queue, final @Nullable String messageSelector, final BrowserCallback action) throws JmsException { Assert.notNull(action, "Callback object must not be null"); @@ -1050,8 +1013,7 @@ public T browseSelected(final Queue queue, @Nullable final String messageSel } @Override - @Nullable - public T browseSelected(final String queueName, @Nullable final String messageSelector, final BrowserCallback action) + public @Nullable T browseSelected(final String queueName, final @Nullable String messageSelector, final BrowserCallback action) throws JmsException { Assert.notNull(action, "Callback object must not be null"); @@ -1075,8 +1037,7 @@ public T browseSelected(final String queueName, @Nullable final String messa * @return an appropriate Connection fetched from the holder, * or {@code null} if none found */ - @Nullable - protected Connection getConnection(JmsResourceHolder holder) { + protected @Nullable Connection getConnection(JmsResourceHolder holder) { return holder.getConnection(); } @@ -1087,8 +1048,7 @@ protected Connection getConnection(JmsResourceHolder holder) { * @return an appropriate Session fetched from the holder, * or {@code null} if none found */ - @Nullable - protected Session getSession(JmsResourceHolder holder) { + protected @Nullable Session getSession(JmsResourceHolder holder) { return holder.getSession(); } @@ -1193,14 +1153,12 @@ protected QueueBrowser createBrowser(Session session, Queue queue, @Nullable Str private class JmsTemplateResourceFactory implements ConnectionFactoryUtils.ResourceFactory { @Override - @Nullable - public Connection getConnection(JmsResourceHolder holder) { + public @Nullable Connection getConnection(JmsResourceHolder holder) { return JmsTemplate.this.getConnection(holder); } @Override - @Nullable - public Session getSession(JmsResourceHolder holder) { + public @Nullable Session getSession(JmsResourceHolder holder) { return JmsTemplate.this.getSession(holder); } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/ProducerCallback.java b/spring-jms/src/main/java/org/springframework/jms/core/ProducerCallback.java index 952d3a6d15a0..e86c19980153 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/ProducerCallback.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/ProducerCallback.java @@ -19,8 +19,7 @@ import jakarta.jms.JMSException; import jakarta.jms.MessageProducer; import jakarta.jms.Session; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Callback for sending a message to a JMS destination. @@ -52,7 +51,6 @@ public interface ProducerCallback { * (or {@code null} if none) * @throws jakarta.jms.JMSException if thrown by JMS API methods */ - @Nullable - T doInJms(Session session, MessageProducer producer) throws JMSException; + @Nullable T doInJms(Session session, MessageProducer producer) throws JMSException; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/SessionCallback.java b/spring-jms/src/main/java/org/springframework/jms/core/SessionCallback.java index 99dfe64b0ca8..1357b006e093 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/SessionCallback.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/SessionCallback.java @@ -18,8 +18,7 @@ import jakarta.jms.JMSException; import jakarta.jms.Session; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Callback for executing any number of operations on a provided {@link Session}. @@ -43,7 +42,6 @@ public interface SessionCallback { * (or {@code null} if none) * @throws jakarta.jms.JMSException if thrown by JMS API methods */ - @Nullable - T doInJms(Session session) throws JMSException; + @Nullable T doInJms(Session session) throws JMSException; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/package-info.java b/spring-jms/src/main/java/org/springframework/jms/core/package-info.java index 4beb013c5759..1270b14b6c84 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/package-info.java @@ -2,9 +2,7 @@ * Core package of the JMS support. * Provides a JmsTemplate class and various callback interfaces. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.core; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java b/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java index 627e5fc311b1..99de379a7327 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/support/JmsGatewaySupport.java @@ -19,11 +19,11 @@ import jakarta.jms.ConnectionFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.jms.core.JmsTemplate; -import org.springframework.lang.Nullable; /** * Convenient superclass for application classes that need JMS access. @@ -45,8 +45,7 @@ public abstract class JmsGatewaySupport implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private JmsTemplate jmsTemplate; + private @Nullable JmsTemplate jmsTemplate; /** @@ -75,8 +74,7 @@ protected JmsTemplate createJmsTemplate(ConnectionFactory connectionFactory) { /** * Return the JMS ConnectionFactory used by the gateway. */ - @Nullable - public final ConnectionFactory getConnectionFactory() { + public final @Nullable ConnectionFactory getConnectionFactory() { return (this.jmsTemplate != null ? this.jmsTemplate.getConnectionFactory() : null); } @@ -91,8 +89,7 @@ public final void setJmsTemplate(@Nullable JmsTemplate jmsTemplate) { /** * Return the JmsTemplate for the gateway. */ - @Nullable - public final JmsTemplate getJmsTemplate() { + public final @Nullable JmsTemplate getJmsTemplate() { return this.jmsTemplate; } diff --git a/spring-jms/src/main/java/org/springframework/jms/core/support/package-info.java b/spring-jms/src/main/java/org/springframework/jms/core/support/package-info.java index 55535f37a3b4..dc74a7fea9be 100644 --- a/spring-jms/src/main/java/org/springframework/jms/core/support/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/core/support/package-info.java @@ -2,9 +2,7 @@ * Classes supporting the {@code org.springframework.jms.core} package. * Contains a base class for JmsTemplate usage. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.core.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java index 8ed0f74fe252..ad4fedb93c74 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractJmsListeningContainer.java @@ -25,6 +25,7 @@ import jakarta.jms.Connection; import jakarta.jms.JMSException; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanNameAware; import org.springframework.beans.factory.DisposableBean; @@ -33,7 +34,6 @@ import org.springframework.jms.connection.ConnectionFactoryUtils; import org.springframework.jms.support.JmsUtils; import org.springframework.jms.support.destination.JmsDestinationAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -65,18 +65,15 @@ public abstract class AbstractJmsListeningContainer extends JmsDestinationAccessor implements BeanNameAware, DisposableBean, SmartLifecycle { - @Nullable - private String clientId; + private @Nullable String clientId; private boolean autoStartup = true; private int phase = DEFAULT_PHASE; - @Nullable - private String beanName; + private @Nullable String beanName; - @Nullable - private Connection sharedConnection; + private @Nullable Connection sharedConnection; private boolean sharedConnectionStarted = false; @@ -110,8 +107,7 @@ public void setClientId(@Nullable String clientId) { * Return the JMS client ID for the shared Connection created and used * by this container, if any. */ - @Nullable - public String getClientId() { + public @Nullable String getClientId() { return this.clientId; } @@ -158,8 +154,7 @@ public void setBeanName(@Nullable String beanName) { * Return the bean name that this listener container has been assigned * in its containing bean factory, if any. */ - @Nullable - protected final String getBeanName() { + protected final @Nullable String getBeanName() { return this.beanName; } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java index f803fc3aa11e..906a53c5ebf5 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractMessageListenerContainer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,11 +33,11 @@ import jakarta.jms.Queue; import jakarta.jms.Session; import jakarta.jms.Topic; +import org.jspecify.annotations.Nullable; import org.springframework.jms.support.JmsUtils; import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ErrorHandler; @@ -152,43 +152,36 @@ public abstract class AbstractMessageListenerContainer extends AbstractJmsListen implements MessageListenerContainer { private static final boolean micrometerJakartaPresent = ClassUtils.isPresent( - "io.micrometer.jakarta9.instrument.jms.JmsInstrumentation", AbstractMessageListenerContainer.class.getClassLoader()); + "io.micrometer.jakarta9.instrument.jms.JmsInstrumentation", + AbstractMessageListenerContainer.class.getClassLoader()); - @Nullable - private volatile Object destination; + private volatile @Nullable Object destination; - @Nullable - private volatile String messageSelector; + private volatile @Nullable String messageSelector; - @Nullable - private volatile Object messageListener; + private volatile @Nullable Object messageListener; private boolean subscriptionDurable = false; private boolean subscriptionShared = false; - @Nullable - private String subscriptionName; + private @Nullable String subscriptionName; - @Nullable - private Boolean replyPubSubDomain; + private boolean pubSubNoLocal = false; - @Nullable - private QosSettings replyQosSettings; + private @Nullable Boolean replyPubSubDomain; - private boolean pubSubNoLocal = false; + private @Nullable QosSettings replyQosSettings; - @Nullable - private MessageConverter messageConverter; + private @Nullable MessageConverter messageConverter; - @Nullable - private ExceptionListener exceptionListener; + private @Nullable ExceptionListener exceptionListener; - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; - @Nullable - private ObservationRegistry observationRegistry; + private @Nullable ObservationRegistry observationRegistry; + + private boolean acknowledgeAfterListener = true; private boolean exposeListenerSession = true; @@ -223,8 +216,7 @@ public void setDestination(@Nullable Destination destination) { * if the configured destination is not an actual {@link Destination} type; * c.f. {@link #setDestinationName(String) when the destination is a String}. */ - @Nullable - public Destination getDestination() { + public @Nullable Destination getDestination() { return (this.destination instanceof Destination _destination ? _destination : null); } @@ -249,8 +241,7 @@ public void setDestinationName(@Nullable String destinationName) { * {@link String} type; c.f. {@link #setDestination(Destination) when * it is an actual Destination}. */ - @Nullable - public String getDestinationName() { + public @Nullable String getDestinationName() { return (this.destination instanceof String name ? name : null); } @@ -279,8 +270,7 @@ public void setMessageSelector(@Nullable String messageSelector) { /** * Return the JMS message selector expression (or {@code null} if none). */ - @Nullable - public String getMessageSelector() { + public @Nullable String getMessageSelector() { return this.messageSelector; } @@ -309,8 +299,7 @@ public void setMessageListener(@Nullable Object messageListener) { /** * Return the message listener object to register. */ - @Nullable - public Object getMessageListener() { + public @Nullable Object getMessageListener() { return this.messageListener; } @@ -428,8 +417,7 @@ public void setSubscriptionName(@Nullable String subscriptionName) { * Return the name of a subscription to create, if any. * @since 4.1 */ - @Nullable - public String getSubscriptionName() { + public @Nullable String getSubscriptionName() { return this.subscriptionName; } @@ -455,8 +443,7 @@ public void setDurableSubscriptionName(@Nullable String durableSubscriptionName) /** * Return the name of a durable subscription to create, if any. */ - @Nullable - public String getDurableSubscriptionName() { + public @Nullable String getDurableSubscriptionName() { return (this.subscriptionDurable ? this.subscriptionName : null); } @@ -500,12 +487,7 @@ public void setReplyPubSubDomain(boolean replyPubSubDomain) { */ @Override public boolean isReplyPubSubDomain() { - if (this.replyPubSubDomain != null) { - return this.replyPubSubDomain; - } - else { - return isPubSubDomain(); - } + return (this.replyPubSubDomain != null ? this.replyPubSubDomain : isPubSubDomain()); } /** @@ -520,8 +502,7 @@ public void setReplyQosSettings(@Nullable QosSettings replyQosSettings) { } @Override - @Nullable - public QosSettings getReplyQosSettings() { + public @Nullable QosSettings getReplyQosSettings() { return this.replyQosSettings; } @@ -534,8 +515,7 @@ public void setMessageConverter(@Nullable MessageConverter messageConverter) { } @Override - @Nullable - public MessageConverter getMessageConverter() { + public @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -551,8 +531,7 @@ public void setExceptionListener(@Nullable ExceptionListener exceptionListener) * Return the JMS ExceptionListener to notify in case of a JMSException thrown * by the registered message listener or the invocation infrastructure, if any. */ - @Nullable - public ExceptionListener getExceptionListener() { + public @Nullable ExceptionListener getExceptionListener() { return this.exceptionListener; } @@ -571,8 +550,7 @@ public void setErrorHandler(@Nullable ErrorHandler errorHandler) { * thrown while processing a {@link Message}. * @since 4.1 */ - @Nullable - public ErrorHandler getErrorHandler() { + public @Nullable ErrorHandler getErrorHandler() { return this.errorHandler; } @@ -591,11 +569,41 @@ public void setObservationRegistry(@Nullable ObservationRegistry observationRegi * {@link JmsObservationDocumentation#JMS_MESSAGE_PROCESS JMS message processing observations}. * @since 6.1 */ - @Nullable - public ObservationRegistry getObservationRegistry() { + public @Nullable ObservationRegistry getObservationRegistry() { return this.observationRegistry; } + /** + * Specify whether the listener container should automatically acknowledge + * each JMS Message after the message listener returned. This applies in + * case of client acknowledge modes, including vendor-specific modes but + * not in case of auto-acknowledge or a transacted JMS Session. + *

    As of 6.2, the default is {@code true}: The listener container will + * acknowledge each JMS Message even in case of a vendor-specific mode, + * assuming client-acknowledge style processing for custom vendor modes. + *

    If the provided listener prefers to manually acknowledge each message in + * the listener itself, in combination with an "individual acknowledge" mode, + * switch this flag to {code false} along with the vendor-specific mode. + * @since 6.2.6 + * @see #setSessionAcknowledgeMode + * @see #setMessageListener + * @see Message#acknowledge() + */ + public void setAcknowledgeAfterListener(boolean acknowledgeAfterListener) { + this.acknowledgeAfterListener = acknowledgeAfterListener; + } + + /** + * Determine whether the listener container should automatically acknowledge + * each JMS Message after the message listener returned. + * @since 6.2.6 + * @see #setAcknowledgeAfterListener + * @see #isClientAcknowledge(Session) + */ + public boolean isAcknowledgeAfterListener() { + return this.acknowledgeAfterListener; + } + /** * Set whether to expose the listener JMS Session to a registered * {@link SessionAwareMessageListener} as well as to @@ -833,7 +841,7 @@ protected void commitIfNecessary(Session session, @Nullable Message message) thr JmsUtils.commitIfNecessary(session); } } - else if (message != null && isClientAcknowledge(session)) { + else if (message != null && isAcknowledgeAfterListener() && isClientAcknowledge(session)) { message.acknowledge(); } } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java index 71c4ad360de3..9de98b612761 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/AbstractPollingMessageListenerContainer.java @@ -23,12 +23,12 @@ import jakarta.jms.Message; import jakarta.jms.MessageConsumer; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.jms.connection.ConnectionFactoryUtils; import org.springframework.jms.connection.JmsResourceHolder; import org.springframework.jms.connection.SingleConnectionFactory; import org.springframework.jms.support.JmsUtils; -import org.springframework.lang.Nullable; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionException; import org.springframework.transaction.TransactionStatus; @@ -89,8 +89,7 @@ public abstract class AbstractPollingMessageListenerContainer extends AbstractMe private boolean sessionTransactedCalled = false; - @Nullable - private PlatformTransactionManager transactionManager; + private @Nullable PlatformTransactionManager transactionManager; private final DefaultTransactionDefinition transactionDefinition = new DefaultTransactionDefinition(); @@ -133,8 +132,7 @@ public void setTransactionManager(@Nullable PlatformTransactionManager transacti * Return the Spring PlatformTransactionManager to use for transactional * wrapping of message receipt plus listener execution. */ - @Nullable - protected final PlatformTransactionManager getTransactionManager() { + protected final @Nullable PlatformTransactionManager getTransactionManager() { return this.transactionManager; } @@ -436,8 +434,7 @@ private void rollbackOnException(PlatformTransactionManager manager, Transaction * @return the Message, or {@code null} if none * @throws JMSException if thrown by JMS methods */ - @Nullable - protected Message receiveMessage(MessageConsumer consumer) throws JMSException { + protected @Nullable Message receiveMessage(MessageConsumer consumer) throws JMSException { return receiveFromConsumer(consumer, getReceiveTimeout()); } @@ -468,8 +465,7 @@ protected void noMessageReceived(Object invoker, Session session) { * @return an appropriate Connection fetched from the holder, * or {@code null} if none found */ - @Nullable - protected Connection getConnection(JmsResourceHolder holder) { + protected @Nullable Connection getConnection(JmsResourceHolder holder) { return holder.getConnection(); } @@ -480,8 +476,7 @@ protected Connection getConnection(JmsResourceHolder holder) { * @return an appropriate Session fetched from the holder, * or {@code null} if none found */ - @Nullable - protected Session getSession(JmsResourceHolder holder) { + protected @Nullable Session getSession(JmsResourceHolder holder) { return holder.getSession(); } @@ -492,14 +487,12 @@ protected Session getSession(JmsResourceHolder holder) { private class MessageListenerContainerResourceFactory implements ConnectionFactoryUtils.ResourceFactory { @Override - @Nullable - public Connection getConnection(JmsResourceHolder holder) { + public @Nullable Connection getConnection(JmsResourceHolder holder) { return AbstractPollingMessageListenerContainer.this.getConnection(holder); } @Override - @Nullable - public Session getSession(JmsResourceHolder holder) { + public @Nullable Session getSession(JmsResourceHolder holder) { return AbstractPollingMessageListenerContainer.this.getSession(holder); } 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 91975cb88b71..ac26a7089dce 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 @@ -28,6 +28,7 @@ import jakarta.jms.JMSException; import jakarta.jms.MessageConsumer; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.core.task.SimpleAsyncTaskExecutor; import org.springframework.core.task.TaskExecutor; @@ -35,7 +36,6 @@ import org.springframework.jms.support.JmsUtils; import org.springframework.jms.support.destination.CachingDestinationResolver; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; import org.springframework.scheduling.SchedulingAwareRunnable; import org.springframework.scheduling.SchedulingTaskExecutor; import org.springframework.util.Assert; @@ -190,8 +190,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe ); - @Nullable - private Executor taskExecutor; + private @Nullable Executor taskExecutor; private boolean virtualThreads = false; @@ -221,8 +220,7 @@ public class DefaultMessageListenerContainer extends AbstractPollingMessageListe private volatile boolean interrupted; - @Nullable - private Runnable stopCallback; + private @Nullable Runnable stopCallback; private Object currentRecoveryMarker = new Object(); @@ -1246,14 +1244,11 @@ public final boolean isRecovering() { */ private class AsyncMessageListenerInvoker implements SchedulingAwareRunnable { - @Nullable - private Session session; + private @Nullable Session session; - @Nullable - private MessageConsumer consumer; + private @Nullable MessageConsumer consumer; - @Nullable - private Object lastRecoveryMarker; + private @Nullable Object lastRecoveryMarker; private boolean lastMessageSucceeded; @@ -1261,8 +1256,7 @@ private class AsyncMessageListenerInvoker implements SchedulingAwareRunnable { private volatile boolean idle = true; - @Nullable - private volatile Thread currentReceiveThread; + private volatile @Nullable Thread currentReceiveThread; @Override public void run() { @@ -1440,7 +1434,7 @@ private void decreaseActiveInvokerCount() { } } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void initResourcesIfNecessary() throws JMSException { if (getCacheLevel() <= CACHE_CONNECTION) { updateRecoveryMarker(); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java index a1fd33b3b1d5..90766c96ccba 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/MessageListenerContainer.java @@ -16,11 +16,12 @@ package org.springframework.jms.listener; +import org.jspecify.annotations.Nullable; + import org.springframework.context.SmartLifecycle; import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; /** * Internal abstraction used by the framework representing a message @@ -42,15 +43,13 @@ public interface MessageListenerContainer extends SmartLifecycle { * Return the {@link MessageConverter} that can be used to * convert {@link jakarta.jms.Message}, if any. */ - @Nullable - MessageConverter getMessageConverter(); + @Nullable MessageConverter getMessageConverter(); /** * Return the {@link DestinationResolver} to use to resolve * destinations by names. */ - @Nullable - DestinationResolver getDestinationResolver(); + @Nullable DestinationResolver getDestinationResolver(); /** * Return whether the Publish/Subscribe domain ({@link jakarta.jms.Topic Topics}) is used. @@ -71,7 +70,6 @@ public interface MessageListenerContainer extends SmartLifecycle { * or {@code null} if the broker's defaults should be used. * @since 5.0 */ - @Nullable - QosSettings getReplyQosSettings(); + @Nullable QosSettings getReplyQosSettings(); } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java index e445eb34e1c4..de3c0c5ba99f 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/SimpleMessageListenerContainer.java @@ -30,9 +30,9 @@ import jakarta.jms.Message; import jakarta.jms.MessageConsumer; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.jms.support.JmsUtils; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -73,14 +73,11 @@ public class SimpleMessageListenerContainer extends AbstractMessageListenerConta private int concurrentConsumers = 1; - @Nullable - private Executor taskExecutor; + private @Nullable Executor taskExecutor; - @Nullable - private Set sessions; + private @Nullable Set sessions; - @Nullable - private Set consumers; + private @Nullable Set consumers; private final Lock consumersLock = new ReentrantLock(); @@ -315,7 +312,7 @@ protected void initializeConsumers() throws JMSException { * @throws JMSException if thrown by JMS methods * @see #executeListener */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Lambda protected MessageConsumer createListenerConsumer(final Session session) throws JMSException { Destination destination = getDestination(); if (destination == null) { @@ -344,7 +341,7 @@ protected MessageConsumer createListenerConsumer(final Session session) throws J * @see #executeListener * @see #setExposeListenerSession */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void processMessage(Message message, Session session) { ConnectionFactory connectionFactory = getConnectionFactory(); boolean exposeResource = (connectionFactory != null && isExposeListenerSession()); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java index 8bb8d92c5ca1..fbfebabf9f76 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/AbstractAdaptableMessageListener.java @@ -26,6 +26,7 @@ import jakarta.jms.Session; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jms.listener.SessionAwareMessageListener; import org.springframework.jms.support.JmsHeaderMapper; @@ -39,7 +40,6 @@ import org.springframework.jms.support.converter.SmartMessageConverter; import org.springframework.jms.support.destination.DestinationResolver; import org.springframework.jms.support.destination.DynamicDestinationResolver; -import org.springframework.lang.Nullable; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; @@ -59,18 +59,15 @@ public abstract class AbstractAdaptableMessageListener /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Object defaultResponseDestination; + private @Nullable Object defaultResponseDestination; private DestinationResolver destinationResolver = new DynamicDestinationResolver(); - @Nullable - private MessageConverter messageConverter = new SimpleMessageConverter(); + private @Nullable MessageConverter messageConverter = new SimpleMessageConverter(); private final MessagingMessageConverterAdapter messagingMessageConverter = new MessagingMessageConverterAdapter(); - @Nullable - private QosSettings responseQosSettings; + private @Nullable QosSettings responseQosSettings; /** @@ -151,8 +148,7 @@ public void setMessageConverter(@Nullable MessageConverter messageConverter) { * listener method arguments, and objects returned from listener * methods back to JMS messages. */ - @Nullable - protected MessageConverter getMessageConverter() { + protected @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } @@ -190,8 +186,7 @@ public void setResponseQosSettings(@Nullable QosSettings responseQosSettings) { * or {@code null} if the defaults should be used. * @since 5.0 */ - @Nullable - protected QosSettings getResponseQosSettings() { + protected @Nullable QosSettings getResponseQosSettings() { return this.responseQosSettings; } @@ -405,8 +400,7 @@ protected Destination getResponseDestination(Message request, Message response, * @see #setDefaultResponseTopicName * @see #setDestinationResolver */ - @Nullable - protected Destination resolveDefaultResponseDestination(Session session) throws JMSException { + protected @Nullable Destination resolveDefaultResponseDestination(Session session) throws JMSException { if (this.defaultResponseDestination instanceof Destination destination) { return destination; } @@ -504,11 +498,9 @@ protected class LazyResolutionMessage implements org.springframework.messaging.M private final jakarta.jms.Message message; - @Nullable - private Object payload; + private @Nullable Object payload; - @Nullable - private MessageHeaders headers; + private @Nullable MessageHeaders headers; public LazyResolutionMessage(jakarta.jms.Message message) { this.message = message; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/JmsResponse.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/JmsResponse.java index 0bd717501649..e0ce32085ca3 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/JmsResponse.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/JmsResponse.java @@ -19,9 +19,9 @@ import jakarta.jms.Destination; import jakarta.jms.JMSException; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -87,8 +87,7 @@ public T getResponse() { * @return the {@link Destination} to use * @throws JMSException if the DestinationResolver failed to resolve the destination */ - @Nullable - public Destination resolveDestination(DestinationResolver destinationResolver, Session session) + public @Nullable Destination resolveDestination(DestinationResolver destinationResolver, Session session) throws JMSException { if (this.destination instanceof Destination dest) { diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java index f30c2e4b8e43..ca4b1aff0582 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessageListenerAdapter.java @@ -22,12 +22,12 @@ import jakarta.jms.Message; import jakarta.jms.MessageListener; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.jms.listener.SessionAwareMessageListener; import org.springframework.jms.listener.SubscriptionNameProvider; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.converter.SimpleMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MethodInvoker; import org.springframework.util.ObjectUtils; @@ -284,8 +284,7 @@ protected Object[] buildListenerArguments(Object extractedMessage) { * @see #getListenerMethodName * @see #buildListenerArguments */ - @Nullable - protected Object invokeListenerMethod(String methodName, Object[] arguments) throws JMSException { + protected @Nullable Object invokeListenerMethod(String methodName, Object[] arguments) throws JMSException { try { MethodInvoker methodInvoker = new MethodInvoker(); methodInvoker.setTargetObject(getDelegate()); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java index 6518a5cd51d9..973abcba992d 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/MessagingMessageListenerAdapter.java @@ -18,12 +18,12 @@ import jakarta.jms.JMSException; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; import org.springframework.jms.listener.SubscriptionNameProvider; import org.springframework.jms.support.JmsHeaderMapper; import org.springframework.jms.support.converter.MessageConversionException; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.AbstractMessageSendingTemplate; @@ -57,8 +57,7 @@ public class MessagingMessageListenerAdapter extends AbstractAdaptableMessageListener implements SubscriptionNameProvider { - @Nullable - private InvocableHandlerMethod handlerMethod; + private @Nullable InvocableHandlerMethod handlerMethod; /** @@ -103,8 +102,7 @@ protected Message toMessagingMessage(jakarta.jms.Message jmsMessage) { * Invoke the handler, wrapping any exception in a {@link ListenerExecutionFailedException} * with a dedicated error message. */ - @Nullable - private Object invokeHandler(jakarta.jms.Message jmsMessage, @Nullable Session session, Message message) { + private @Nullable Object invokeHandler(jakarta.jms.Message jmsMessage, @Nullable Session session, Message message) { InvocableHandlerMethod handlerMethod = getHandlerMethod(); try { return handlerMethod.invoke(message, jmsMessage, session); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/package-info.java b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/package-info.java index 57f5b0228924..bb8b651007d8 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/adapter/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/adapter/package-info.java @@ -3,9 +3,7 @@ * methods, converting messages to appropriate message content types * (such as String or byte array) that get passed into listener methods. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.listener.adapter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java index ec1d22184b5f..d6c7f1b54125 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsActivationSpecConfig.java @@ -19,10 +19,10 @@ import java.util.Map; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -55,29 +55,23 @@ public class JmsActivationSpecConfig { ); - @Nullable - private String destinationName; + private @Nullable String destinationName; private boolean pubSubDomain = false; - @Nullable - private Boolean replyPubSubDomain; + private @Nullable Boolean replyPubSubDomain; - @Nullable - private QosSettings replyQosSettings; + private @Nullable QosSettings replyQosSettings; private boolean subscriptionDurable = false; private boolean subscriptionShared = false; - @Nullable - private String subscriptionName; + private @Nullable String subscriptionName; - @Nullable - private String clientId; + private @Nullable String clientId; - @Nullable - private String messageSelector; + private @Nullable String messageSelector; private int acknowledgeMode = Session.AUTO_ACKNOWLEDGE; @@ -85,16 +79,14 @@ public class JmsActivationSpecConfig { private int prefetchSize = -1; - @Nullable - private MessageConverter messageConverter; + private @Nullable MessageConverter messageConverter; public void setDestinationName(@Nullable String destinationName) { this.destinationName = destinationName; } - @Nullable - public String getDestinationName() { + public @Nullable String getDestinationName() { return this.destinationName; } @@ -123,8 +115,7 @@ public void setReplyQosSettings(@Nullable QosSettings replyQosSettings) { this.replyQosSettings = replyQosSettings; } - @Nullable - public QosSettings getReplyQosSettings() { + public @Nullable QosSettings getReplyQosSettings() { return this.replyQosSettings; } @@ -154,8 +145,7 @@ public void setSubscriptionName(@Nullable String subscriptionName) { this.subscriptionName = subscriptionName; } - @Nullable - public String getSubscriptionName() { + public @Nullable String getSubscriptionName() { return this.subscriptionName; } @@ -164,8 +154,7 @@ public void setDurableSubscriptionName(@Nullable String durableSubscriptionName) this.subscriptionDurable = (durableSubscriptionName != null); } - @Nullable - public String getDurableSubscriptionName() { + public @Nullable String getDurableSubscriptionName() { return (this.subscriptionDurable ? this.subscriptionName : null); } @@ -173,8 +162,7 @@ public void setClientId(@Nullable String clientId) { this.clientId = clientId; } - @Nullable - public String getClientId() { + public @Nullable String getClientId() { return this.clientId; } @@ -182,8 +170,7 @@ public void setMessageSelector(@Nullable String messageSelector) { this.messageSelector = messageSelector; } - @Nullable - public String getMessageSelector() { + public @Nullable String getMessageSelector() { return this.messageSelector; } @@ -297,8 +284,7 @@ public void setMessageConverter(@Nullable MessageConverter messageConverter) { /** * Return the {@link MessageConverter} to use, if any. */ - @Nullable - public MessageConverter getMessageConverter() { + public @Nullable MessageConverter getMessageConverter() { return this.messageConverter; } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java index 6a164236d715..7769ec8351d1 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointFactory.java @@ -20,9 +20,9 @@ import jakarta.jms.MessageListener; import jakarta.resource.ResourceException; import jakarta.resource.spi.UnavailableException; +import org.jspecify.annotations.Nullable; import org.springframework.jca.endpoint.AbstractMessageEndpointFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -49,8 +49,7 @@ */ public class JmsMessageEndpointFactory extends AbstractMessageEndpointFactory { - @Nullable - private MessageListener messageListener; + private @Nullable MessageListener messageListener; /** diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java index 64e380f8137b..4dc7290ae4ea 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/JmsMessageEndpointManager.java @@ -18,6 +18,7 @@ import jakarta.jms.MessageListener; import jakarta.resource.ResourceException; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanNameAware; import org.springframework.jca.endpoint.GenericMessageEndpointManager; @@ -25,7 +26,6 @@ import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; /** * Extension of the generic JCA 1.5 @@ -60,8 +60,7 @@ public class JmsMessageEndpointManager extends GenericMessageEndpointManager private JmsActivationSpecFactory activationSpecFactory = new DefaultJmsActivationSpecFactory(); - @Nullable - private JmsActivationSpecConfig activationSpecConfig; + private @Nullable JmsActivationSpecConfig activationSpecConfig; /** @@ -146,8 +145,7 @@ public void setActivationSpecConfig(@Nullable JmsActivationSpecConfig activation * Return the {@link JmsActivationSpecConfig} object that this endpoint manager * should use for activating its listener. Return {@code null} if none is set. */ - @Nullable - public JmsActivationSpecConfig getActivationSpecConfig() { + public @Nullable JmsActivationSpecConfig getActivationSpecConfig() { return this.activationSpecConfig; } @@ -191,8 +189,7 @@ public void setupMessageListener(Object messageListener) { } @Override - @Nullable - public MessageConverter getMessageConverter() { + public @Nullable MessageConverter getMessageConverter() { JmsActivationSpecConfig config = getActivationSpecConfig(); if (config != null) { return config.getMessageConverter(); @@ -201,8 +198,7 @@ public MessageConverter getMessageConverter() { } @Override - @Nullable - public DestinationResolver getDestinationResolver() { + public @Nullable DestinationResolver getDestinationResolver() { if (this.activationSpecFactory instanceof StandardJmsActivationSpecFactory standardFactory) { return standardFactory.getDestinationResolver(); } @@ -228,8 +224,7 @@ public boolean isReplyPubSubDomain() { } @Override - @Nullable - public QosSettings getReplyQosSettings() { + public @Nullable QosSettings getReplyQosSettings() { JmsActivationSpecConfig config = getActivationSpecConfig(); if (config != null) { return config.getReplyQosSettings(); diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java index 81232764d511..62d312cc050b 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/StandardJmsActivationSpecFactory.java @@ -24,13 +24,13 @@ import jakarta.jms.Topic; import jakarta.resource.spi.ActivationSpec; import jakarta.resource.spi.ResourceAdapter; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapper; import org.springframework.beans.PropertyAccessorFactory; import org.springframework.jms.support.destination.DestinationResolutionException; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; /** * Standard implementation of the {@link JmsActivationSpecFactory} interface. @@ -52,14 +52,11 @@ */ public class StandardJmsActivationSpecFactory implements JmsActivationSpecFactory { - @Nullable - private Class activationSpecClass; + private @Nullable Class activationSpecClass; - @Nullable - private Map defaultProperties; + private @Nullable Map defaultProperties; - @Nullable - private DestinationResolver destinationResolver; + private @Nullable DestinationResolver destinationResolver; /** @@ -98,8 +95,7 @@ public void setDestinationResolver(@Nullable DestinationResolver destinationReso /** * Return the {@link DestinationResolver} to use for resolving destinations names. */ - @Nullable - public DestinationResolver getDestinationResolver() { + public @Nullable DestinationResolver getDestinationResolver() { return this.destinationResolver; } @@ -131,8 +127,7 @@ public ActivationSpec createActivationSpec(ResourceAdapter adapter, JmsActivatio * if not determinable * @see #setActivationSpecClass */ - @Nullable - protected Class determineActivationSpecClass(ResourceAdapter adapter) { + protected @Nullable Class determineActivationSpecClass(ResourceAdapter adapter) { return null; } diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/package-info.java b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/package-info.java index 23a0e183cd40..98a6278d912c 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/endpoint/package-info.java @@ -1,9 +1,7 @@ /** * This package provides JCA-based endpoint management for JMS message listeners. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.listener.endpoint; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/listener/package-info.java b/spring-jms/src/main/java/org/springframework/jms/listener/package-info.java index 2652ddc6f189..c7b7c404f4d2 100644 --- a/spring-jms/src/main/java/org/springframework/jms/listener/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/listener/package-info.java @@ -3,9 +3,7 @@ * It also offers the DefaultMessageListenerContainer and SimpleMessageListenerContainer * implementations, based on the plain JMS client API. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.listener; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/package-info.java b/spring-jms/src/main/java/org/springframework/jms/package-info.java index e114d09c017c..5c68e46f6604 100644 --- a/spring-jms/src/main/java/org/springframework/jms/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/package-info.java @@ -2,9 +2,7 @@ * This package contains integration classes for JMS, * allowing for Spring-style JMS access. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java index 5145749e7fb8..f963c9aebf38 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsAccessor.java @@ -24,10 +24,10 @@ import jakarta.jms.Session; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.jms.JmsException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,8 +63,7 @@ public abstract class JmsAccessor implements InitializingBean { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; private boolean sessionTransacted = false; @@ -82,8 +81,7 @@ public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) * Return the ConnectionFactory that this accessor uses for obtaining * JMS {@link Connection Connections}. */ - @Nullable - public ConnectionFactory getConnectionFactory() { + public @Nullable ConnectionFactory getConnectionFactory() { return this.connectionFactory; } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java index 3523c7808ba6..17534797b58a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsMessageHeaderAccessor.java @@ -20,8 +20,8 @@ import java.util.Map; import jakarta.jms.Destination; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.NativeMessageHeaderAccessor; @@ -47,8 +47,7 @@ protected JmsMessageHeaderAccessor(Message message) { * Return the {@link JmsHeaders#CORRELATION_ID correlationId}. * @see JmsHeaders#CORRELATION_ID */ - @Nullable - public String getCorrelationId() { + public @Nullable String getCorrelationId() { return (String) getHeader(JmsHeaders.CORRELATION_ID); } @@ -56,8 +55,7 @@ public String getCorrelationId() { * Return the {@link JmsHeaders#DESTINATION destination}. * @see JmsHeaders#DESTINATION */ - @Nullable - public Destination getDestination() { + public @Nullable Destination getDestination() { return (Destination) getHeader(JmsHeaders.DESTINATION); } @@ -65,8 +63,7 @@ public Destination getDestination() { * Return the {@link JmsHeaders#DELIVERY_MODE delivery mode}. * @see JmsHeaders#DELIVERY_MODE */ - @Nullable - public Integer getDeliveryMode() { + public @Nullable Integer getDeliveryMode() { return (Integer) getHeader(JmsHeaders.DELIVERY_MODE); } @@ -74,8 +71,7 @@ public Integer getDeliveryMode() { * Return the message {@link JmsHeaders#EXPIRATION expiration}. * @see JmsHeaders#EXPIRATION */ - @Nullable - public Long getExpiration() { + public @Nullable Long getExpiration() { return (Long) getHeader(JmsHeaders.EXPIRATION); } @@ -83,8 +79,7 @@ public Long getExpiration() { * Return the {@link JmsHeaders#MESSAGE_ID message id}. * @see JmsHeaders#MESSAGE_ID */ - @Nullable - public String getMessageId() { + public @Nullable String getMessageId() { return (String) getHeader(JmsHeaders.MESSAGE_ID); } @@ -92,8 +87,7 @@ public String getMessageId() { * Return the {@link JmsHeaders#PRIORITY priority}. * @see JmsHeaders#PRIORITY */ - @Nullable - public Integer getPriority() { + public @Nullable Integer getPriority() { return (Integer) getHeader(JmsHeaders.PRIORITY); } @@ -101,8 +95,7 @@ public Integer getPriority() { * Return the {@link JmsHeaders#REPLY_TO reply to}. * @see JmsHeaders#REPLY_TO */ - @Nullable - public Destination getReplyTo() { + public @Nullable Destination getReplyTo() { return (Destination) getHeader(JmsHeaders.REPLY_TO); } @@ -110,8 +103,7 @@ public Destination getReplyTo() { * Return the {@link JmsHeaders#REDELIVERED redelivered} flag. * @see JmsHeaders#REDELIVERED */ - @Nullable - public Boolean getRedelivered() { + public @Nullable Boolean getRedelivered() { return (Boolean) getHeader(JmsHeaders.REDELIVERED); } @@ -119,8 +111,7 @@ public Boolean getRedelivered() { * Return the {@link JmsHeaders#TYPE type}. * @see JmsHeaders#TYPE */ - @Nullable - public String getType() { + public @Nullable String getType() { return (String) getHeader(JmsHeaders.TYPE); } @@ -129,8 +120,7 @@ public String getType() { * @see JmsHeaders#TIMESTAMP */ @Override - @Nullable - public Long getTimestamp() { + public @Nullable Long getTimestamp() { return (Long) getHeader(JmsHeaders.TIMESTAMP); } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java b/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java index b4815924fa10..d5a88a35458a 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/JmsUtils.java @@ -25,6 +25,7 @@ import jakarta.jms.Session; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jms.InvalidClientIDException; import org.springframework.jms.InvalidDestinationException; @@ -39,7 +40,6 @@ import org.springframework.jms.TransactionInProgressException; import org.springframework.jms.TransactionRolledBackException; import org.springframework.jms.UncategorizedJmsException; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -241,8 +241,7 @@ public static void rollbackIfNecessary(Session session) throws JMSException { * @return the descriptive message String * @see jakarta.jms.JMSException#getLinkedException() */ - @Nullable - public static String buildExceptionMessage(JMSException ex) { + public static @Nullable String buildExceptionMessage(JMSException ex) { String message = ex.getMessage(); Exception linkedEx = ex.getLinkedException(); if (linkedEx != null) { diff --git a/spring-jms/src/main/java/org/springframework/jms/support/QosSettings.java b/spring-jms/src/main/java/org/springframework/jms/support/QosSettings.java index 39ee533489c4..117566562786 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/QosSettings.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/QosSettings.java @@ -17,8 +17,7 @@ package org.springframework.jms.support; import jakarta.jms.Message; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Gather the Quality-of-Service settings that can be used when sending a message. 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 000000000000..75867407bd96 --- /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 50e1099a4091..e8449c3c7a1f 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 @@ -35,10 +35,10 @@ import jakarta.jms.Message; import jakarta.jms.Session; import jakarta.jms.TextMessage; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -60,7 +60,9 @@ * @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 { /** @@ -73,21 +75,17 @@ public class MappingJackson2MessageConverter implements SmartMessageConverter, B private MessageType targetType = MessageType.BYTES; - @Nullable - private String encoding; + private @Nullable String encoding; - @Nullable - private String encodingPropertyName; + private @Nullable String encodingPropertyName; - @Nullable - private String typeIdPropertyName; + private @Nullable String typeIdPropertyName; private Map> idClassMappings = new HashMap<>(); private final Map, String> classIdMappings = new HashMap<>(); - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; /** @@ -478,8 +476,7 @@ protected JavaType getJavaTypeForMessage(Message message) throws JMSException { * converter for the current conversion attempt * @return the serialization view class, or {@code null} if none */ - @Nullable - protected Class getSerializationView(@Nullable Object conversionHint) { + protected @Nullable Class getSerializationView(@Nullable Object conversionHint) { if (conversionHint instanceof MethodParameter methodParam) { JsonView annotation = methodParam.getParameterAnnotation(JsonView.class); if (annotation == null) { diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MarshallingMessageConverter.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MarshallingMessageConverter.java index afea54c559c2..1bada096ffd4 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MarshallingMessageConverter.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MarshallingMessageConverter.java @@ -32,9 +32,9 @@ import jakarta.jms.Message; import jakarta.jms.Session; import jakarta.jms.TextMessage; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.oxm.Marshaller; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; @@ -52,11 +52,9 @@ */ public class MarshallingMessageConverter implements MessageConverter, InitializingBean { - @Nullable - private Marshaller marshaller; + private @Nullable Marshaller marshaller; - @Nullable - private Unmarshaller unmarshaller; + private @Nullable Unmarshaller unmarshaller; private MessageType targetType = MessageType.BYTES; diff --git a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java index e6ba699751d0..142a6e4be831 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/MessageConversionException.java @@ -16,8 +16,9 @@ package org.springframework.jms.support.converter; +import org.jspecify.annotations.Nullable; + import org.springframework.jms.JmsException; -import org.springframework.lang.Nullable; /** * Thrown by {@link MessageConverter} implementations when the conversion 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 188ff2348428..750f3a951ecc 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. @@ -20,11 +20,11 @@ import jakarta.jms.JMSException; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.jms.support.JmsHeaderMapper; import org.springframework.jms.support.SimpleJmsHeaderMapper; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.core.AbstractMessageSendingTemplate; @@ -132,7 +132,7 @@ protected Object extractPayload(jakarta.jms.Message message) throws JMSException /** * 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 3a6468d78c83..9d21509a0313 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. @@ -19,8 +19,7 @@ import jakarta.jms.JMSException; import jakarta.jms.Message; import jakarta.jms.Session; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * An extended {@link MessageConverter} SPI with conversion hint support. @@ -41,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/main/java/org/springframework/jms/support/converter/package-info.java b/spring-jms/src/main/java/org/springframework/jms/support/converter/package-info.java index 53c46520b704..97a800b9b435 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/converter/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/converter/package-info.java @@ -2,9 +2,7 @@ * Provides a MessageConverter abstraction to convert * between Java objects and JMS messages. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.support.converter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java index 291e7a3d0790..0bd70301939d 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/BeanFactoryDestinationResolver.java @@ -19,11 +19,11 @@ import jakarta.jms.Destination; import jakarta.jms.JMSException; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -38,8 +38,7 @@ */ public class BeanFactoryDestinationResolver implements DestinationResolver, BeanFactoryAware { - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java index ce3e74f1cdab..63faba406595 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolutionException.java @@ -16,8 +16,9 @@ package org.springframework.jms.support.destination; +import org.jspecify.annotations.Nullable; + import org.springframework.jms.JmsException; -import org.springframework.lang.Nullable; /** * Thrown by a DestinationResolver when it cannot resolve a destination name. diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java index ddc273729ab6..c7a8f178a2d4 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/DestinationResolver.java @@ -19,8 +19,7 @@ import jakarta.jms.Destination; import jakarta.jms.JMSException; import jakarta.jms.Session; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy interface for resolving JMS destinations. diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java index 0a1d885f7ef5..331e6457ceb2 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/DynamicDestinationResolver.java @@ -21,8 +21,8 @@ import jakarta.jms.Queue; import jakarta.jms.Session; import jakarta.jms.Topic; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java index 6711e7030f72..15fdc84e69a7 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/JmsDestinationAccessor.java @@ -21,9 +21,9 @@ import jakarta.jms.Message; import jakarta.jms.MessageConsumer; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.springframework.jms.support.JmsAccessor; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -126,8 +126,7 @@ protected Destination resolveDestinationName(Session session, String destination * @see #RECEIVE_TIMEOUT_NO_WAIT * @see #RECEIVE_TIMEOUT_INDEFINITE_WAIT */ - @Nullable - protected Message receiveFromConsumer(MessageConsumer consumer, long timeout) throws JMSException { + protected @Nullable Message receiveFromConsumer(MessageConsumer consumer, long timeout) throws JMSException { if (timeout > 0) { return consumer.receive(timeout); } diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java index b27d883a0884..ae9b29e4b667 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/JndiDestinationResolver.java @@ -26,9 +26,9 @@ import jakarta.jms.Queue; import jakarta.jms.Session; import jakarta.jms.Topic; +import org.jspecify.annotations.Nullable; import org.springframework.jndi.JndiLocatorSupport; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** diff --git a/spring-jms/src/main/java/org/springframework/jms/support/destination/package-info.java b/spring-jms/src/main/java/org/springframework/jms/support/destination/package-info.java index 91b3c3e50211..b7b8c73afc1f 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/destination/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/destination/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for Spring's JMS framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.support.destination; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/main/java/org/springframework/jms/support/package-info.java b/spring-jms/src/main/java/org/springframework/jms/support/package-info.java index 34433006917d..e85dac873ecb 100644 --- a/spring-jms/src/main/java/org/springframework/jms/support/package-info.java +++ b/spring-jms/src/main/java/org/springframework/jms/support/package-info.java @@ -2,9 +2,7 @@ * This package provides generic JMS support classes, * to be used by higher-level classes like JmsTemplate. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.jms.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java b/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java index b158f8c3464a..b592219d2a8e 100644 --- a/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/annotation/AbstractJmsAnnotationDrivenTests.java @@ -19,6 +19,7 @@ import java.lang.reflect.Method; import jakarta.jms.JMSException; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -30,7 +31,6 @@ import org.springframework.jms.config.SimpleJmsListenerEndpoint; import org.springframework.jms.listener.SimpleMessageListenerContainer; import org.springframework.jms.listener.adapter.MessagingMessageListenerAdapter; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.SendTo; import org.springframework.stereotype.Component; import org.springframework.util.ReflectionUtils; diff --git a/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java b/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java index 6fde3769d90c..ed146ca0f908 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/config/JmsNamespaceHandlerTests.java @@ -293,10 +293,10 @@ void testComponentRegistration() { assertThat(context.containsComponentDefinition("listener1")).as("Parser should have registered a component named 'listener1'").isTrue(); assertThat(context.containsComponentDefinition("listener2")).as("Parser should have registered a component named 'listener2'").isTrue(); assertThat(context.containsComponentDefinition("listener3")).as("Parser should have registered a component named 'listener3'").isTrue(); - assertThat(context.containsComponentDefinition(DefaultMessageListenerContainer.class.getName() + "#0")).as("Parser should have registered a component named '" - + DefaultMessageListenerContainer.class.getName() + "#0'").isTrue(); - assertThat(context.containsComponentDefinition(JmsMessageEndpointManager.class.getName() + "#0")).as("Parser should have registered a component named '" - + JmsMessageEndpointManager.class.getName() + "#0'").isTrue(); + assertThat(context.containsComponentDefinition(DefaultMessageListenerContainer.class.getName() + "#0")).as("Parser should have registered a component named '" + + DefaultMessageListenerContainer.class.getName() + "#0'").isTrue(); + assertThat(context.containsComponentDefinition(JmsMessageEndpointManager.class.getName() + "#0")).as("Parser should have registered a component named '" + + JmsMessageEndpointManager.class.getName() + "#0'").isTrue(); assertThat(context.containsComponentDefinition("testJmsFactory")).as("Parser should have registered a component named 'testJmsFactory").isTrue(); assertThat(context.containsComponentDefinition("testJcaFactory")).as("Parser should have registered a component named 'testJcaFactory").isTrue(); assertThat(context.containsComponentDefinition("onlyJmsFactory")).as("Parser should have registered a component named 'testJcaFactory").isTrue(); diff --git a/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java b/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java index 7ce757365866..18128bd1df99 100644 --- a/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/config/MethodJmsListenerEndpointTests.java @@ -29,6 +29,7 @@ import jakarta.jms.QueueSender; 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.junit.jupiter.api.TestInfo; @@ -47,7 +48,6 @@ import org.springframework.jms.support.QosSettings; import org.springframework.jms.support.converter.MessageConverter; import org.springframework.jms.support.destination.DestinationResolver; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.converter.MessageConversionException; diff --git a/spring-jms/src/test/java/org/springframework/jms/core/JmsTemplateTests.java b/spring-jms/src/test/java/org/springframework/jms/core/JmsTemplateTests.java index 290c4c9c58a8..4440b71d48db 100644 --- a/spring-jms/src/test/java/org/springframework/jms/core/JmsTemplateTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/core/JmsTemplateTests.java @@ -530,8 +530,7 @@ private void doTestReceive( if (!useTransactedTemplate() && !useTransactedSession()) { given(this.session.getAcknowledgeMode()).willReturn( - clientAcknowledge ? Session.CLIENT_ACKNOWLEDGE - : Session.AUTO_ACKNOWLEDGE); + clientAcknowledge ? Session.CLIENT_ACKNOWLEDGE : Session.AUTO_ACKNOWLEDGE); } TextMessage textMessage = mock(); diff --git a/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java b/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java index 82f5f08918ba..bffebc7d6957 100644 --- a/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java +++ b/spring-jms/src/test/java/org/springframework/jms/listener/SimpleMessageListenerContainerTests.java @@ -27,11 +27,11 @@ import jakarta.jms.MessageConsumer; import jakarta.jms.MessageListener; import jakarta.jms.Session; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.context.support.GenericApplicationContext; import org.springframework.jms.StubQueue; -import org.springframework.lang.Nullable; import org.springframework.util.ErrorHandler; import static org.assertj.core.api.Assertions.assertThat; 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 3ab88d2c92a9..44916da6f45c 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.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 @@ void replyPayloadNoDestination() throws JMSException { @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 @@ public TextMessage testReplyWithJackson(String methodName, String replyContent) 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 @@ protected Object extractMessage(jakarta.jms.Message message) { 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 000000000000..1416fa3ab04f --- /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 3dee45d9b93e..a7be4fdf7849 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. @@ -29,12 +29,12 @@ 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 org.springframework.lang.Nullable; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; @@ -49,6 +49,7 @@ * @author Dave Syer * @author Stephane Nicoll */ +@SuppressWarnings("removal") class MappingJackson2MessageConverterTests { private MappingJackson2MessageConverter converter = new MappingJackson2MessageConverter(); diff --git a/spring-messaging/spring-messaging.gradle b/spring-messaging/spring-messaging.gradle index 6f50cf5989bd..a611cf669825 100644 --- a/spring-messaging/spring-messaging.gradle +++ b/spring-messaging/spring-messaging.gradle @@ -6,19 +6,21 @@ apply plugin: "kotlinx-serialization" dependencies { api(project(":spring-beans")) api(project(":spring-core")) + compileOnly("jakarta.validation:jakarta.validation-api") + compileOnly("com.google.code.findbugs:jsr305") // for Reactor optional(project(":spring-context")) optional(project(":spring-oxm")) optional("com.fasterxml.jackson.core:jackson-databind") optional("com.google.code.gson:gson") optional("com.google.protobuf:protobuf-java-util") optional("io.projectreactor.netty:reactor-netty-http") - optional("io.projectreactor.netty:reactor-netty5-http") optional("io.rsocket:rsocket-core") optional("io.rsocket:rsocket-transport-netty") optional("jakarta.json.bind:jakarta.json.bind-api") 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/MessageDeliveryException.java b/spring-messaging/src/main/java/org/springframework/messaging/MessageDeliveryException.java index f73fafb8960e..9a1ad8dcbeb3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/MessageDeliveryException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/MessageDeliveryException.java @@ -16,7 +16,7 @@ package org.springframework.messaging; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Exception that indicates an error occurred during message delivery. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java b/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java index 5d0f0d2c85fe..c7cdb761e7f2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/MessageHeaders.java @@ -30,8 +30,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.AlternativeJdkIdGenerator; import org.springframework.util.CollectionUtils; import org.springframework.util.IdGenerator; @@ -112,8 +112,7 @@ public class MessageHeaders implements Map, Serializable { private static final IdGenerator defaultIdGenerator = new AlternativeJdkIdGenerator(); - @Nullable - private static volatile IdGenerator idGenerator; + private static volatile @Nullable IdGenerator idGenerator; @SuppressWarnings("serial") private final Map headers; @@ -183,30 +182,25 @@ protected static IdGenerator getIdGenerator() { return (generator != null ? generator : defaultIdGenerator); } - @Nullable - public UUID getId() { + public @Nullable UUID getId() { return get(ID, UUID.class); } - @Nullable - public Long getTimestamp() { + public @Nullable Long getTimestamp() { return get(TIMESTAMP, Long.class); } - @Nullable - public Object getReplyChannel() { + public @Nullable Object getReplyChannel() { return get(REPLY_CHANNEL); } - @Nullable - public Object getErrorChannel() { + public @Nullable Object getErrorChannel() { return get(ERROR_CHANNEL); } @SuppressWarnings("unchecked") - @Nullable - public T get(Object key, Class type) { + public @Nullable T get(Object key, Class type) { Object value = this.headers.get(key); if (value == null) { return null; @@ -237,8 +231,7 @@ public Set> entrySet() { } @Override - @Nullable - public Object get(Object key) { + public @Nullable Object get(Object key) { return this.headers.get(key); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/MessagingException.java b/spring-messaging/src/main/java/org/springframework/messaging/MessagingException.java index bd2f8d26af1d..35822ce03144 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/MessagingException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/MessagingException.java @@ -16,8 +16,9 @@ package org.springframework.messaging; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; /** * The base exception for any failures related to messaging. @@ -29,8 +30,7 @@ @SuppressWarnings("serial") public class MessagingException extends NestedRuntimeException { - @Nullable - private final Message failedMessage; + private final @Nullable Message failedMessage; public MessagingException(Message message) { @@ -64,15 +64,14 @@ public MessagingException(Message message, @Nullable String description, @Nul } - @Nullable - public Message getFailedMessage() { + public @Nullable Message getFailedMessage() { return this.failedMessage; } @Override public String toString() { - return super.toString() + (this.failedMessage == null ? "" - : (", failedMessage=" + this.failedMessage)); + return super.toString() + (this.failedMessage == null ? "" : + (", failedMessage=" + this.failedMessage)); } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/PollableChannel.java b/spring-messaging/src/main/java/org/springframework/messaging/PollableChannel.java index 1dc0ba778074..8bedc2814ffe 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/PollableChannel.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/PollableChannel.java @@ -16,7 +16,7 @@ package org.springframework.messaging; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@link MessageChannel} from which messages may be actively received through polling. @@ -30,8 +30,7 @@ public interface PollableChannel extends MessageChannel { * Receive a message from this channel, blocking indefinitely if necessary. * @return the next available {@link Message} or {@code null} if interrupted */ - @Nullable - Message receive(); + @Nullable Message receive(); /** * Receive a message from this channel, blocking until either a message is available @@ -40,7 +39,6 @@ public interface PollableChannel extends MessageChannel { * @return the next available {@link Message} or {@code null} if the specified timeout * period elapses or the message receipt is interrupted */ - @Nullable - Message receive(long timeout); + @Nullable Message receive(long timeout); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractJsonMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractJsonMessageConverter.java index d3032dc5ad06..a837d6fc2f0e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractJsonMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/AbstractJsonMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,6 +18,7 @@ import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.OutputStreamWriter; @@ -27,7 +28,8 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.ClassUtils; @@ -61,8 +63,7 @@ protected boolean supports(Class clazz) { } @Override - @Nullable - protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { + protected @Nullable Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { try { Type resolvedType = getResolvedType(targetClass, conversionHint); Object payload = message.getPayload(); @@ -83,15 +84,13 @@ else if (payload instanceof byte[] bytes) { } @Override - @Nullable - protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { + protected @Nullable Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { try { Type resolvedType = getResolvedType(payload.getClass(), conversionHint); if (byte[].class == getSerializedPayloadClass()) { ByteArrayOutputStream out = new ByteArrayOutputStream(1024); Writer writer = getWriter(out, headers); toJson(payload, resolvedType, writer); - writer.flush(); return out.toByteArray(); } else { @@ -120,11 +119,11 @@ private Charset getCharsetToUse(@Nullable MessageHeaders headers) { } - protected abstract Object fromJson(Reader reader, Type resolvedType); + protected abstract Object fromJson(Reader reader, Type resolvedType) throws IOException; protected abstract Object fromJson(String payload, Type resolvedType); - protected abstract void toJson(Object payload, Type resolvedType, Writer writer); + protected abstract void toJson(Object payload, Type resolvedType, Writer writer) throws IOException; protected abstract String toJson(Object payload, Type resolvedType); 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 60c88c0ea9a8..3c4cffcfaf53 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. @@ -25,10 +25,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.GenericTypeResolver; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; @@ -53,8 +53,7 @@ public abstract class AbstractMessageConverter implements SmartMessageConverter private final List supportedMimeTypes = new ArrayList<>(4); - @Nullable - private ContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(); + private @Nullable ContentTypeResolver contentTypeResolver = new DefaultContentTypeResolver(); private boolean strictContentTypeMatch = false; @@ -116,8 +115,7 @@ public void setContentTypeResolver(@Nullable ContentTypeResolver resolver) { * Return the {@link #setContentTypeResolver(ContentTypeResolver) configured} * {@code ContentTypeResolver}. */ - @Nullable - public ContentTypeResolver getContentTypeResolver() { + public @Nullable ContentTypeResolver getContentTypeResolver() { return this.contentTypeResolver; } @@ -169,14 +167,12 @@ public Class getSerializedPayloadClass() { @Override - @Nullable - public final Object fromMessage(Message message, Class targetClass) { + public final @Nullable Object fromMessage(Message message, Class targetClass) { return fromMessage(message, targetClass, null); } @Override - @Nullable - public final Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint) { + public final @Nullable Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint) { if (!canConvertFrom(message, targetClass)) { return null; } @@ -184,14 +180,12 @@ public final Object fromMessage(Message message, Class targetClass, @Nulla } @Override - @Nullable - public final Message toMessage(Object payload, @Nullable MessageHeaders headers) { + public final @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers) { return toMessage(payload, headers, null); } @Override - @Nullable - public final Message toMessage( + public final @Nullable Message toMessage( Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { if (!canConvertTo(payload, headers)) { @@ -249,8 +243,7 @@ protected boolean supportsMimeType(@Nullable MessageHeaders headers) { return false; } - @Nullable - protected MimeType getMimeType(@Nullable MessageHeaders headers) { + protected @Nullable MimeType getMimeType(@Nullable MessageHeaders headers) { return (this.contentTypeResolver != null ? this.contentTypeResolver.resolve(headers) : null); } @@ -264,8 +257,7 @@ protected MimeType getMimeType(@Nullable MessageHeaders headers) { * @param payload the payload being converted to a message * @return the content type, or {@code null} if not known */ - @Nullable - protected MimeType getDefaultContentType(Object payload) { + protected @Nullable MimeType getDefaultContentType(Object payload) { List mimeTypes = getSupportedMimeTypes(); return (!mimeTypes.isEmpty() ? mimeTypes.get(0) : null); } @@ -283,13 +275,12 @@ protected MimeType getDefaultContentType(Object payload) { * @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 */ - @Nullable - protected Object convertFromInternal( + protected @Nullable Object convertFromInternal( Message message, Class targetClass, @Nullable Object conversionHint) { return null; @@ -300,13 +291,12 @@ protected Object convertFromInternal( * @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 */ - @Nullable - protected Object convertToInternal( + protected @Nullable Object convertToInternal( Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { return null; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/ByteArrayMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/ByteArrayMessageConverter.java index 0a26c82f0dad..8762b6cab46e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/ByteArrayMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/ByteArrayMessageConverter.java @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.MimeTypeUtils; @@ -41,16 +42,14 @@ protected boolean supports(Class clazz) { } @Override - @Nullable - protected Object convertFromInternal( + protected @Nullable Object convertFromInternal( Message message, @Nullable Class targetClass, @Nullable Object conversionHint) { return message.getPayload(); } @Override - @Nullable - protected Object convertToInternal( + protected @Nullable Object convertToInternal( Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { return payload; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/CompositeMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/CompositeMessageConverter.java index 0c46db923453..cc803975dcc0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/CompositeMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/CompositeMessageConverter.java @@ -20,7 +20,8 @@ import java.util.Collection; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; @@ -51,8 +52,7 @@ public CompositeMessageConverter(Collection converters) { @Override - @Nullable - public Object fromMessage(Message message, Class targetClass) { + public @Nullable Object fromMessage(Message message, Class targetClass) { for (MessageConverter converter : getConverters()) { Object result = converter.fromMessage(message, targetClass); if (result != null) { @@ -63,8 +63,7 @@ public Object fromMessage(Message message, Class targetClass) { } @Override - @Nullable - public Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint) { + public @Nullable Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint) { for (MessageConverter converter : getConverters()) { Object result = (converter instanceof SmartMessageConverter smartMessageConverter ? smartMessageConverter.fromMessage(message, targetClass, conversionHint) : @@ -77,8 +76,7 @@ public Object fromMessage(Message message, Class targetClass, @Nullable Ob } @Override - @Nullable - public Message toMessage(Object payload, @Nullable MessageHeaders headers) { + public @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers) { for (MessageConverter converter : getConverters()) { Message result = converter.toMessage(payload, headers); if (result != null) { @@ -89,8 +87,7 @@ public Message toMessage(Object payload, @Nullable MessageHeaders headers) { } @Override - @Nullable - public Message toMessage(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { + public @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { for (MessageConverter converter : getConverters()) { Message result = (converter instanceof SmartMessageConverter smartMessageConverter ? smartMessageConverter.toMessage(payload, headers, conversionHint) : diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/ContentTypeResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/ContentTypeResolver.java index b116e8bdb8a4..61440abbe0fb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/ContentTypeResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/ContentTypeResolver.java @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessageHeaders; import org.springframework.util.InvalidMimeTypeException; import org.springframework.util.MimeType; @@ -37,7 +38,6 @@ public interface ContentTypeResolver { * @throws InvalidMimeTypeException if the content type is a String that cannot be parsed * @throws IllegalArgumentException if there is a content type but its type is unknown */ - @Nullable - MimeType resolve(@Nullable MessageHeaders headers) throws InvalidMimeTypeException; + @Nullable MimeType resolve(@Nullable MessageHeaders headers) throws InvalidMimeTypeException; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/DefaultContentTypeResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/DefaultContentTypeResolver.java index 8dca8058c291..77bba894611b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/DefaultContentTypeResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/DefaultContentTypeResolver.java @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessageHeaders; import org.springframework.util.MimeType; @@ -32,8 +33,7 @@ */ public class DefaultContentTypeResolver implements ContentTypeResolver { - @Nullable - private MimeType defaultMimeType; + private @Nullable MimeType defaultMimeType; /** @@ -49,15 +49,13 @@ public void setDefaultMimeType(@Nullable MimeType defaultMimeType) { * Return the default MIME type to use if no * {@link MessageHeaders#CONTENT_TYPE} header is present. */ - @Nullable - public MimeType getDefaultMimeType() { + public @Nullable MimeType getDefaultMimeType() { return this.defaultMimeType; } @Override - @Nullable - public MimeType resolve(@Nullable MessageHeaders headers) { + public @Nullable MimeType resolve(@Nullable MessageHeaders headers) { if (headers == null || headers.get(MessageHeaders.CONTENT_TYPE) == null) { return this.defaultMimeType; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java index fbe8165b98db..1b55d55b5368 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/GenericMessageConverter.java @@ -16,10 +16,11 @@ package org.springframework.messaging.converter; +import org.jspecify.annotations.Nullable; + import org.springframework.core.convert.ConversionException; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -58,8 +59,7 @@ public GenericMessageConverter(ConversionService conversionService) { @Override - @Nullable - public Object fromMessage(Message message, Class targetClass) { + public @Nullable Object fromMessage(Message message, Class targetClass) { Object payload = message.getPayload(); if (this.conversionService.canConvert(payload.getClass(), targetClass)) { try { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/GsonMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/GsonMessageConverter.java index 95de58ede268..0e15dfc09b75 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/GsonMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/GsonMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,6 +16,7 @@ package org.springframework.messaging.converter; +import java.io.IOException; import java.io.Reader; import java.io.Writer; import java.lang.reflect.ParameterizedType; @@ -88,13 +89,14 @@ protected Object fromJson(String payload, Type resolvedType) { } @Override - protected void toJson(Object payload, Type resolvedType, Writer writer) { + protected void toJson(Object payload, Type resolvedType, Writer writer) throws IOException { if (resolvedType instanceof ParameterizedType) { getGson().toJson(payload, resolvedType, writer); } else { getGson().toJson(payload, writer); } + writer.flush(); } @Override 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 000000000000..8a48d0f3515d --- /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/KotlinSerializationJsonMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java index 44e4aa830f3a..75decdb84589 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/KotlinSerializationJsonMessageConverter.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -76,13 +76,9 @@ protected Object fromJson(String payload, Type resolvedType) { } @Override - protected void toJson(Object payload, Type resolvedType, Writer writer) { - try { - writer.write(toJson(payload, resolvedType).toCharArray()); - } - catch (IOException ex) { - throw new MessageConversionException("Could not write JSON: " + ex.getMessage(), ex); - } + protected void toJson(Object payload, Type resolvedType, Writer writer) throws IOException { + writer.write(toJson(payload, resolvedType).toCharArray()); + writer.flush(); } @Override @@ -106,4 +102,5 @@ private KSerializer serializer(Type type) { } return serializer; } + } 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 d43e991870fb..819bd2343a33 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 @@ -34,9 +34,9 @@ import com.fasterxml.jackson.databind.MapperFeature; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; @@ -56,7 +56,9 @@ * @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[] { @@ -64,8 +66,7 @@ public class MappingJackson2MessageConverter extends AbstractMessageConverter { private ObjectMapper objectMapper; - @Nullable - private Boolean prettyPrint; + private @Nullable Boolean prettyPrint; /** @@ -202,8 +203,8 @@ protected void logWarningIfNecessary(Type type, @Nullable Throwable cause) { } // Do not log warning for serializer not found (note: different message wording on Jackson 2.9) - boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage() != null - && cause.getMessage().startsWith("Cannot find")); + boolean debugLevel = (cause instanceof JsonMappingException && cause.getMessage() != null && + cause.getMessage().startsWith("Cannot find")); if (debugLevel ? logger.isDebugEnabled() : logger.isWarnEnabled()) { String msg = "Failed to evaluate Jackson " + (type instanceof JavaType ? "de" : "") + @@ -227,8 +228,7 @@ protected boolean supports(Class clazz) { } @Override - @Nullable - protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { + 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); @@ -260,8 +260,7 @@ else if (payload instanceof byte[] bytes) { } @Override - @Nullable - protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, + protected @Nullable Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { try { @@ -304,8 +303,7 @@ protected Object convertToInternal(Object payload, @Nullable MessageHeaders head * @return the serialization view class, or {@code null} if none * @since 4.2 */ - @Nullable - protected Class getSerializationView(@Nullable Object conversionHint) { + 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)); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java index 0821559db609..92653d96ce82 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MarshallingMessageConverter.java @@ -27,8 +27,9 @@ import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.TypeMismatchException; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.oxm.Marshaller; @@ -51,11 +52,9 @@ */ public class MarshallingMessageConverter extends AbstractMessageConverter { - @Nullable - private Marshaller marshaller; + private @Nullable Marshaller marshaller; - @Nullable - private Unmarshaller unmarshaller; + private @Nullable Unmarshaller unmarshaller; /** @@ -102,8 +101,7 @@ public void setMarshaller(@Nullable Marshaller marshaller) { /** * Return the configured Marshaller. */ - @Nullable - public Marshaller getMarshaller() { + public @Nullable Marshaller getMarshaller() { return this.marshaller; } @@ -117,8 +115,7 @@ public void setUnmarshaller(@Nullable Unmarshaller unmarshaller) { /** * Return the configured unmarshaller. */ - @Nullable - public Unmarshaller getUnmarshaller() { + public @Nullable Unmarshaller getUnmarshaller() { return this.unmarshaller; } @@ -142,8 +139,7 @@ protected boolean supports(Class clazz) { } @Override - @Nullable - protected Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { + protected @Nullable Object convertFromInternal(Message message, Class targetClass, @Nullable Object conversionHint) { Assert.state(this.unmarshaller != null, "Property 'unmarshaller' is required"); try { Source source = getSource(message.getPayload()); @@ -168,8 +164,7 @@ private Source getSource(Object payload) { } @Override - @Nullable - protected Object convertToInternal(Object payload, @Nullable MessageHeaders headers, + protected @Nullable Object convertToInternal(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { Assert.state(this.marshaller != null, "Property 'marshaller' is required"); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConversionException.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConversionException.java index 3a6922a2fca6..68ab72732553 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConversionException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConversionException.java @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConverter.java index 84d95e6d7d4a..3de08c007ec0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/MessageConverter.java @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -42,8 +43,7 @@ public interface MessageConverter { * @return the result of the conversion, or {@code null} if the converter cannot * perform the conversion */ - @Nullable - Object fromMessage(Message message, Class targetClass); + @Nullable Object fromMessage(Message message, Class targetClass); /** * Create a {@link Message} whose payload is the result of converting the given @@ -58,7 +58,6 @@ public interface MessageConverter { * @return the new message, or {@code null} if the converter does not support the * Object type or the target media type */ - @Nullable - Message toMessage(Object payload, @Nullable MessageHeaders headers); + @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufJsonFormatMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufJsonFormatMessageConverter.java index 62fa2c620651..17f300fd2d5e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufJsonFormatMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufJsonFormatMessageConverter.java @@ -18,8 +18,7 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.util.JsonFormat; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Subclass of {@link ProtobufMessageConverter} for use with the official @@ -52,7 +51,7 @@ public ProtobufJsonFormatMessageConverter(@Nullable ExtensionRegistry extensionR * JsonFormat.Printer}, and a default instance of {@link ExtensionRegistry}. */ public ProtobufJsonFormatMessageConverter( - @Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { + JsonFormat.@Nullable Parser parser, JsonFormat.@Nullable Printer printer) { this(parser, printer, null); } @@ -62,8 +61,8 @@ public ProtobufJsonFormatMessageConverter( * JsonFormat.Parser}, {@link com.google.protobuf.util.JsonFormat.Printer * JsonFormat.Printer}, and {@link ExtensionRegistry}. */ - public ProtobufJsonFormatMessageConverter(@Nullable JsonFormat.Parser parser, - @Nullable JsonFormat.Printer printer, @Nullable ExtensionRegistry extensionRegistry) { + public ProtobufJsonFormatMessageConverter(JsonFormat.@Nullable Parser parser, + JsonFormat.@Nullable Printer printer, @Nullable ExtensionRegistry extensionRegistry) { super(new ProtobufJavaUtilSupport(parser, printer), extensionRegistry); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java index bab066d23b13..e3ae17469707 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/ProtobufMessageConverter.java @@ -28,8 +28,8 @@ import com.google.protobuf.ExtensionRegistry; import com.google.protobuf.Message; import com.google.protobuf.util.JsonFormat; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.MessageHeaders; import org.springframework.util.ClassUtils; import org.springframework.util.ConcurrentReferenceHashMap; @@ -75,8 +75,7 @@ public class ProtobufMessageConverter extends AbstractMessageConverter { final ExtensionRegistry extensionRegistry; - @Nullable - private final ProtobufFormatSupport protobufFormatSupport; + private final @Nullable ProtobufFormatSupport protobufFormatSupport; /** @@ -244,7 +243,7 @@ static class ProtobufJavaUtilSupport implements ProtobufFormatSupport { private final JsonFormat.Printer printer; - public ProtobufJavaUtilSupport(@Nullable JsonFormat.Parser parser, @Nullable JsonFormat.Printer printer) { + public ProtobufJavaUtilSupport(JsonFormat.@Nullable Parser parser, JsonFormat.@Nullable Printer printer) { this.parser = (parser != null ? parser : JsonFormat.parser()); this.printer = (printer != null ? printer : JsonFormat.printer()); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/SimpleMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/SimpleMessageConverter.java index 9f2407300ce0..ba5fd5040c44 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/SimpleMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/SimpleMessageConverter.java @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageBuilder; @@ -36,8 +37,7 @@ public class SimpleMessageConverter implements MessageConverter { @Override - @Nullable - public Object fromMessage(Message message, Class targetClass) { + public @Nullable Object fromMessage(Message message, Class targetClass) { Object payload = message.getPayload(); return (ClassUtils.isAssignableValue(targetClass, payload) ? payload : null); } 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 cba7ff9c2a6e..68092515623e 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. @@ -16,7 +16,8 @@ package org.springframework.messaging.converter; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -39,13 +40,12 @@ 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) */ - @Nullable - Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint); + @Nullable Object fromMessage(Message message, Class targetClass, @Nullable Object conversionHint); /** * A variant of {@link #toMessage(Object, MessageHeaders)} which takes an extra @@ -54,12 +54,11 @@ 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) */ - @Nullable - Message toMessage(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint); + @Nullable Message toMessage(Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/StringMessageConverter.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/StringMessageConverter.java index 9bcfcc93a4ca..85293e9baf61 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/StringMessageConverter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/StringMessageConverter.java @@ -19,7 +19,8 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; @@ -61,8 +62,7 @@ protected Object convertFromInternal(Message message, Class targetClass, @ } @Override - @Nullable - protected Object convertToInternal( + protected @Nullable Object convertToInternal( Object payload, @Nullable MessageHeaders headers, @Nullable Object conversionHint) { if (byte[].class == getSerializedPayloadClass()) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/converter/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/converter/package-info.java index c5dcb046af75..3a76fc8b7a77 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/converter/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/converter/package-info.java @@ -1,9 +1,7 @@ /** * Provides support for message conversion. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.converter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractDestinationResolvingMessagingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractDestinationResolvingMessagingTemplate.java index d16dda9f5634..3b29c9843429 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractDestinationResolvingMessagingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractDestinationResolvingMessagingTemplate.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.util.Assert; @@ -42,8 +43,7 @@ public abstract class AbstractDestinationResolvingMessagingTemplate extends A DestinationResolvingMessageReceivingOperations, DestinationResolvingMessageRequestReplyOperations { - @Nullable - private DestinationResolver destinationResolver; + private @Nullable DestinationResolver destinationResolver; /** @@ -60,8 +60,7 @@ public void setDestinationResolver(@Nullable DestinationResolver destinationR /** * Return the configured destination resolver. */ - @Nullable - public DestinationResolver getDestinationResolver() { + public @Nullable DestinationResolver getDestinationResolver() { return this.destinationResolver; } @@ -102,36 +101,31 @@ public void convertAndSend(String destinationName, T payload, } @Override - @Nullable - public Message receive(String destinationName) { + public @Nullable Message receive(String destinationName) { D destination = resolveDestination(destinationName); return super.receive(destination); } @Override - @Nullable - public T receiveAndConvert(String destinationName, Class targetClass) { + public @Nullable T receiveAndConvert(String destinationName, Class targetClass) { D destination = resolveDestination(destinationName); return super.receiveAndConvert(destination, targetClass); } @Override - @Nullable - public Message sendAndReceive(String destinationName, Message requestMessage) { + public @Nullable Message sendAndReceive(String destinationName, Message requestMessage) { D destination = resolveDestination(destinationName); return super.sendAndReceive(destination, requestMessage); } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, Class targetClass) { + public @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass) { D destination = resolveDestination(destinationName); return super.convertSendAndReceive(destination, request, targetClass); } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, + public @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass) { D destination = resolveDestination(destinationName); @@ -139,8 +133,7 @@ public T convertSendAndReceive(String destinationName, Object request, } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, Class targetClass, + public @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass, @Nullable MessagePostProcessor postProcessor) { D destination = resolveDestination(destinationName); @@ -148,8 +141,7 @@ public T convertSendAndReceive(String destinationName, Object request, Class } @Override - @Nullable - public T convertSendAndReceive(String destinationName, Object request, + public @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor postProcessor) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageReceivingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageReceivingTemplate.java index ab869001986d..518faee93491 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageReceivingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageReceivingTemplate.java @@ -16,7 +16,8 @@ package org.springframework.messaging.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.converter.MessageConverter; @@ -35,14 +36,12 @@ public abstract class AbstractMessageReceivingTemplate extends AbstractMessag implements MessageReceivingOperations { @Override - @Nullable - public Message receive() { + public @Nullable Message receive() { return doReceive(getRequiredDefaultDestination()); } @Override - @Nullable - public Message receive(D destination) { + public @Nullable Message receive(D destination) { return doReceive(destination); } @@ -52,19 +51,16 @@ public Message receive(D destination) { * @return the received message, possibly {@code null} if the message could not * be received, for example due to a timeout */ - @Nullable - protected abstract Message doReceive(D destination); + protected abstract @Nullable Message doReceive(D destination); @Override - @Nullable - public T receiveAndConvert(Class targetClass) { + public @Nullable T receiveAndConvert(Class targetClass) { return receiveAndConvert(getRequiredDefaultDestination(), targetClass); } @Override - @Nullable - public T receiveAndConvert(D destination, Class targetClass) { + public @Nullable T receiveAndConvert(D destination, Class targetClass) { Message message = doReceive(destination); if (message != null) { return doConvert(message, targetClass); @@ -81,8 +77,7 @@ public T receiveAndConvert(D destination, Class targetClass) { * @return the converted payload of the reply message (never {@code null}) */ @SuppressWarnings("unchecked") - @Nullable - protected T doConvert(Message message, Class targetClass) { + protected @Nullable T doConvert(Message message, Class targetClass) { MessageConverter messageConverter = getMessageConverter(); T value = (T) messageConverter.fromMessage(message, targetClass); if (value == null) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java index 699dabe9e231..9e012166b761 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessageSendingTemplate.java @@ -20,8 +20,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; @@ -53,8 +53,7 @@ public abstract class AbstractMessageSendingTemplate implements MessageSendin protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private D defaultDestination; + private @Nullable D defaultDestination; private MessageConverter converter = new SimpleMessageConverter(); @@ -71,8 +70,7 @@ public void setDefaultDestination(@Nullable D defaultDestination) { /** * Return the configured default destination. */ - @Nullable - public D getDefaultDestination() { + public @Nullable D getDefaultDestination() { return this.defaultDestination; } @@ -194,8 +192,7 @@ protected Message doConvert(Object payload, @Nullable Map hea * @param headers the headers to send (or {@code null} if none) * @return the actual headers to send (or {@code null} if none) */ - @Nullable - protected Map processHeadersToSend(@Nullable Map headers) { + protected @Nullable Map processHeadersToSend(@Nullable Map headers) { return headers; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessagingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessagingTemplate.java index 2814e3512c29..9911e7734e66 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessagingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/AbstractMessagingTemplate.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; /** @@ -35,52 +36,44 @@ public abstract class AbstractMessagingTemplate extends AbstractMessageReceiv implements MessageRequestReplyOperations { @Override - @Nullable - public Message sendAndReceive(Message requestMessage) { + public @Nullable Message sendAndReceive(Message requestMessage) { return sendAndReceive(getRequiredDefaultDestination(), requestMessage); } @Override - @Nullable - public Message sendAndReceive(D destination, Message requestMessage) { + public @Nullable Message sendAndReceive(D destination, Message requestMessage) { return doSendAndReceive(destination, requestMessage); } - @Nullable - protected abstract Message doSendAndReceive(D destination, Message requestMessage); + protected abstract @Nullable Message doSendAndReceive(D destination, Message requestMessage); @Override - @Nullable - public T convertSendAndReceive(Object request, Class targetClass) { + public @Nullable T convertSendAndReceive(Object request, Class targetClass) { return convertSendAndReceive(getRequiredDefaultDestination(), request, targetClass); } @Override - @Nullable - public T convertSendAndReceive(D destination, Object request, Class targetClass) { + public @Nullable T convertSendAndReceive(D destination, Object request, Class targetClass) { return convertSendAndReceive(destination, request, null, targetClass); } @Override - @Nullable - public T convertSendAndReceive( + public @Nullable T convertSendAndReceive( D destination, Object request, @Nullable Map headers, Class targetClass) { return convertSendAndReceive(destination, request, headers, targetClass, null); } @Override - @Nullable - public T convertSendAndReceive( + public @Nullable T convertSendAndReceive( Object request, Class targetClass, @Nullable MessagePostProcessor postProcessor) { return convertSendAndReceive(getRequiredDefaultDestination(), request, targetClass, postProcessor); } @Override - @Nullable - public T convertSendAndReceive(D destination, Object request, Class targetClass, + public @Nullable T convertSendAndReceive(D destination, Object request, Class targetClass, @Nullable MessagePostProcessor postProcessor) { return convertSendAndReceive(destination, request, null, targetClass, postProcessor); @@ -88,8 +81,7 @@ public T convertSendAndReceive(D destination, Object request, Class targe @SuppressWarnings("unchecked") @Override - @Nullable - public T convertSendAndReceive(D destination, Object request, @Nullable Map headers, + public @Nullable T convertSendAndReceive(D destination, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor postProcessor) { Message requestMessage = doConvert(request, headers, postProcessor); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/BeanFactoryMessageChannelDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/core/BeanFactoryMessageChannelDestinationResolver.java index 3ff3cd6826ef..3a189a9a49b8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/BeanFactoryMessageChannelDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/BeanFactoryMessageChannelDestinationResolver.java @@ -16,10 +16,11 @@ package org.springframework.messaging.core; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.messaging.MessageChannel; import org.springframework.util.Assert; @@ -34,8 +35,7 @@ public class BeanFactoryMessageChannelDestinationResolver implements DestinationResolver, BeanFactoryAware { - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/CachingDestinationResolverProxy.java b/spring-messaging/src/main/java/org/springframework/messaging/core/CachingDestinationResolverProxy.java index bf83792123f8..240cb82a8eef 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/CachingDestinationResolverProxy.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/CachingDestinationResolverProxy.java @@ -19,8 +19,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,8 +40,7 @@ public class CachingDestinationResolverProxy implements DestinationResolver resolvedDestinationCache = new ConcurrentHashMap<>(); - @Nullable - private DestinationResolver targetDestinationResolver; + private @Nullable DestinationResolver targetDestinationResolver; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java index 282b4a2de831..17899462d6d2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolutionException.java @@ -16,7 +16,8 @@ package org.springframework.messaging.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessagingException; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageReceivingOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageReceivingOperations.java index b85f20622cb5..035b97dcbaba 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageReceivingOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageReceivingOperations.java @@ -16,7 +16,8 @@ package org.springframework.messaging.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; @@ -36,8 +37,7 @@ public interface DestinationResolvingMessageReceivingOperations extends Messa * Resolve the given destination name and receive a message from it. * @param destinationName the destination name to resolve */ - @Nullable - Message receive(String destinationName) throws MessagingException; + @Nullable Message receive(String destinationName) throws MessagingException; /** * Resolve the given destination name, receive a message from it, @@ -45,7 +45,6 @@ public interface DestinationResolvingMessageReceivingOperations extends Messa * @param destinationName the destination name to resolve * @param targetClass the target class for the converted payload */ - @Nullable - T receiveAndConvert(String destinationName, Class targetClass) throws MessagingException; + @Nullable T receiveAndConvert(String destinationName, Class targetClass) throws MessagingException; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageRequestReplyOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageRequestReplyOperations.java index 16c3e4e4e163..f90dc2235da0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageRequestReplyOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageRequestReplyOperations.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; @@ -42,8 +43,7 @@ public interface DestinationResolvingMessageRequestReplyOperations extends Me * @return the received message, possibly {@code null} if the message could not * be received, for example due to a timeout */ - @Nullable - Message sendAndReceive(String destinationName, Message requestMessage) throws MessagingException; + @Nullable Message sendAndReceive(String destinationName, Message requestMessage) throws MessagingException; /** * Resolve the given destination name, convert the payload request Object @@ -57,8 +57,7 @@ public interface DestinationResolvingMessageRequestReplyOperations extends Me * @return the converted payload of the reply message, possibly {@code null} if * the message could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(String destinationName, Object request, Class targetClass) + @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass) throws MessagingException; /** @@ -74,8 +73,7 @@ T convertSendAndReceive(String destinationName, Object request, Class tar * @return the converted payload of the reply message, possibly {@code null} if * the message could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(String destinationName, Object request, + @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass) throws MessagingException; /** @@ -92,8 +90,7 @@ T convertSendAndReceive(String destinationName, Object request, * @return the converted payload of the reply message, possibly {@code null} if * the message could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(String destinationName, Object request, Class targetClass, + @Nullable T convertSendAndReceive(String destinationName, Object request, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; /** @@ -111,8 +108,7 @@ T convertSendAndReceive(String destinationName, Object request, Class tar * @return the converted payload of the reply message, possibly {@code null} if * the message could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, + @Nullable T convertSendAndReceive(String destinationName, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageSendingOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageSendingOperations.java index 6abe4dda58be..4a40707d7ed4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageSendingOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/DestinationResolvingMessageSendingOperations.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/GenericMessagingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/core/GenericMessagingTemplate.java index 95952f4b0b9f..b2f747f33c81 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/GenericMessagingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/GenericMessagingTemplate.java @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageDeliveryException; @@ -176,8 +176,8 @@ protected final void doSend(MessageChannel channel, Message message, long tim accessor.removeHeader(this.receiveTimeoutHeader); accessor.setImmutable(); } - else if (message.getHeaders().containsKey(this.sendTimeoutHeader) - || message.getHeaders().containsKey(this.receiveTimeoutHeader)) { + else if (message.getHeaders().containsKey(this.sendTimeoutHeader) || + message.getHeaders().containsKey(this.receiveTimeoutHeader)) { messageToSend = MessageBuilder.fromMessage(message) .setHeader(this.sendTimeoutHeader, null) .setHeader(this.receiveTimeoutHeader, null) @@ -193,13 +193,11 @@ else if (message.getHeaders().containsKey(this.sendTimeoutHeader) } @Override - @Nullable - protected final Message doReceive(MessageChannel channel) { + protected final @Nullable Message doReceive(MessageChannel channel) { return doReceive(channel, this.receiveTimeout); } - @Nullable - protected final Message doReceive(MessageChannel channel, long timeout) { + protected final @Nullable Message doReceive(MessageChannel channel, long timeout) { Assert.notNull(channel, "MessageChannel is required"); if (!(channel instanceof PollableChannel pollableChannel)) { throw new IllegalStateException("A PollableChannel is required to receive messages"); @@ -215,8 +213,7 @@ protected final Message doReceive(MessageChannel channel, long timeout) { } @Override - @Nullable - protected final Message doSendAndReceive(MessageChannel channel, Message requestMessage) { + protected final @Nullable Message doSendAndReceive(MessageChannel channel, Message requestMessage) { Assert.notNull(channel, "'channel' is required"); Object originalReplyChannelHeader = requestMessage.getHeaders().getReplyChannel(); Object originalErrorChannelHeader = requestMessage.getHeaders().getErrorChannel(); @@ -259,8 +256,7 @@ private long receiveTimeout(Message requestMessage) { return (receiveTimeout != null ? receiveTimeout : this.receiveTimeout); } - @Nullable - private Long headerToLong(@Nullable Object headerValue) { + private @Nullable Long headerToLong(@Nullable Object headerValue) { if (headerValue instanceof Number number) { return number.longValue(); } @@ -284,8 +280,7 @@ private static final class TemporaryReplyChannel implements PollableChannel { private final boolean throwExceptionOnLateReply; - @Nullable - private volatile Message replyMessage; + private volatile @Nullable Message replyMessage; private volatile boolean hasReceived; @@ -302,14 +297,12 @@ public void setSendFailed(boolean hasSendError) { } @Override - @Nullable - public Message receive() { + public @Nullable Message receive() { return this.receive(-1); } @Override - @Nullable - public Message receive(long timeout) { + public @Nullable Message receive(long timeout) { try { if (timeout < 0) { this.replyLatch.await(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/MessageReceivingOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/core/MessageReceivingOperations.java index 18a1062f2d59..2079db56fd3a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/MessageReceivingOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/MessageReceivingOperations.java @@ -16,7 +16,8 @@ package org.springframework.messaging.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; @@ -36,8 +37,7 @@ public interface MessageReceivingOperations { * @return the received message, possibly {@code null} if the message could not * be received, for example due to a timeout */ - @Nullable - Message receive() throws MessagingException; + @Nullable Message receive() throws MessagingException; /** * Receive a message from the given destination. @@ -45,8 +45,7 @@ public interface MessageReceivingOperations { * @return the received message, possibly {@code null} if the message could not * be received, for example due to a timeout */ - @Nullable - Message receive(D destination) throws MessagingException; + @Nullable Message receive(D destination) throws MessagingException; /** * Receive a message from a default destination and convert its payload to the @@ -55,8 +54,7 @@ public interface MessageReceivingOperations { * @return the converted payload of the reply message, possibly {@code null} if * the message could not be received, for example due to a timeout */ - @Nullable - T receiveAndConvert(Class targetClass) throws MessagingException; + @Nullable T receiveAndConvert(Class targetClass) throws MessagingException; /** * Receive a message from the given destination and convert its payload to the @@ -66,7 +64,6 @@ public interface MessageReceivingOperations { * @return the converted payload of the reply message, possibly {@code null} if * the message could not be received, for example due to a timeout */ - @Nullable - T receiveAndConvert(D destination, Class targetClass) throws MessagingException; + @Nullable T receiveAndConvert(D destination, Class targetClass) throws MessagingException; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/MessageRequestReplyOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/core/MessageRequestReplyOperations.java index 249fa9c66801..83882510dc0a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/MessageRequestReplyOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/MessageRequestReplyOperations.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; @@ -39,8 +40,7 @@ public interface MessageRequestReplyOperations { * @return the reply, possibly {@code null} if the message could not be received, * for example due to a timeout */ - @Nullable - Message sendAndReceive(Message requestMessage) throws MessagingException; + @Nullable Message sendAndReceive(Message requestMessage) throws MessagingException; /** * Send a request message and receive the reply from the given destination. @@ -49,8 +49,7 @@ public interface MessageRequestReplyOperations { * @return the reply, possibly {@code null} if the message could not be received, * for example due to a timeout */ - @Nullable - Message sendAndReceive(D destination, Message requestMessage) throws MessagingException; + @Nullable Message sendAndReceive(D destination, Message requestMessage) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -62,8 +61,7 @@ public interface MessageRequestReplyOperations { * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(Object request, Class targetClass) throws MessagingException; + @Nullable T convertSendAndReceive(Object request, Class targetClass) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -76,8 +74,7 @@ public interface MessageRequestReplyOperations { * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(D destination, Object request, Class targetClass) throws MessagingException; + @Nullable T convertSendAndReceive(D destination, Object request, Class targetClass) throws MessagingException; /** * Convert the given request Object to serialized form, possibly using a @@ -91,8 +88,7 @@ public interface MessageRequestReplyOperations { * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive( + @Nullable T convertSendAndReceive( D destination, Object request, @Nullable Map headers, Class targetClass) throws MessagingException; @@ -108,8 +104,7 @@ T convertSendAndReceive( * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive( + @Nullable T convertSendAndReceive( Object request, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; @@ -126,8 +121,7 @@ T convertSendAndReceive( * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive(D destination, Object request, Class targetClass, + @Nullable T convertSendAndReceive(D destination, Object request, Class targetClass, MessagePostProcessor requestPostProcessor) throws MessagingException; /** @@ -143,8 +137,7 @@ T convertSendAndReceive(D destination, Object request, Class targetClass, * @return the payload of the reply message, possibly {@code null} if the message * could not be received, for example due to a timeout */ - @Nullable - T convertSendAndReceive( + @Nullable T convertSendAndReceive( D destination, Object request, @Nullable Map headers, Class targetClass, @Nullable MessagePostProcessor requestPostProcessor) throws MessagingException; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/MessageSendingOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/core/MessageSendingOperations.java index 93944974a0ff..f961d0dc7957 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/MessageSendingOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/MessageSendingOperations.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/core/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/core/package-info.java index ed87d0e45aea..41eb9cd090fd 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/core/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/core/package-info.java @@ -1,9 +1,7 @@ /** * Defines interfaces and implementation classes for messaging templates. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.core; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/AbstractMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/AbstractMessageCondition.java index 1df91f749549..085ce64ac536 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/AbstractMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/AbstractMessageCondition.java @@ -19,7 +19,7 @@ import java.util.Collection; import java.util.StringJoiner; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Base class for {@code MessageCondition's} that pre-declares abstract methods diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/CompositeMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/CompositeMessageCondition.java index f9785b9096a2..52f42b447e1c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/CompositeMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/CompositeMessageCondition.java @@ -21,7 +21,8 @@ import java.util.List; import java.util.stream.Collectors; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.util.Assert; @@ -80,8 +81,7 @@ private > T combine(MessageCondition first, Mes } @Override - @Nullable - public CompositeMessageCondition getMatchingCondition(Message message) { + public @Nullable CompositeMessageCondition getMatchingCondition(Message message) { List> result = new ArrayList<>(this.messageConditions.size()); for (MessageCondition condition : this.messageConditions) { MessageCondition matchingCondition = (MessageCondition) condition.getMatchingCondition(message); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java index bdb6e23dec39..c42593d2d623 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/DestinationPatternsMessageCondition.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,8 @@ import java.util.List; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.util.AntPathMatcher; import org.springframework.util.CollectionUtils; @@ -160,8 +161,7 @@ else if (!other.patterns.isEmpty()) { * or {@code null} either if a destination can not be extracted or there is no match */ @Override - @Nullable - public DestinationPatternsMessageCondition getMatchingCondition(Message message) { + public @Nullable DestinationPatternsMessageCondition getMatchingCondition(Message message) { Object destination = message.getHeaders().get(LOOKUP_DESTINATION_HEADER); if (destination == null) { return null; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java index 7dfd09c6f4dc..c7116d33b3cf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/HandlerMethod.java @@ -22,10 +22,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.core.annotation.AnnotatedMethod; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -53,13 +53,11 @@ public class HandlerMethod extends AnnotatedMethod { private final Object bean; - @Nullable - private final BeanFactory beanFactory; + private final @Nullable BeanFactory beanFactory; private final Class beanType; - @Nullable - private HandlerMethod resolvedFromHandlerMethod; + private @Nullable HandlerMethod resolvedFromHandlerMethod; protected Log logger = defaultLogger; @@ -167,8 +165,7 @@ protected Class getContainingClass() { * resolved via {@link #createWithResolvedBean()}. * @since 4.3 */ - @Nullable - public HandlerMethod getResolvedFromHandlerMethod() { + public @Nullable HandlerMethod getResolvedFromHandlerMethod() { return this.resolvedFromHandlerMethod; } @@ -196,8 +193,8 @@ public String getShortLogMessage() { @Override public boolean equals(@Nullable Object other) { - return (this == other || (super.equals(other) && other instanceof HandlerMethod otherMethod - && this.bean.equals(otherMethod.bean))); + return (this == other || (super.equals(other) && other instanceof HandlerMethod otherMethod && + this.bean.equals(otherMethod.bean))); } @Override @@ -215,7 +212,7 @@ public int hashCode() { * beans, and others). Endpoint classes that require proxying should prefer * class-based proxy mechanisms. */ - protected void assertTargetBean(Method method, Object targetBean, Object[] args) { + protected void assertTargetBean(Method method, Object targetBean, @Nullable Object[] args) { Class methodDeclaringClass = method.getDeclaringClass(); Class targetBeanClass = targetBean.getClass(); if (!methodDeclaringClass.isAssignableFrom(targetBeanClass)) { @@ -227,7 +224,7 @@ protected void assertTargetBean(Method method, Object targetBean, Object[] args) } } - protected String formatInvokeError(String text, Object[] args) { + protected String formatInvokeError(String text, @Nullable Object[] args) { String formattedArgs = IntStream.range(0, args.length) .mapToObj(i -> (args[i] != null ? "[" + i + "] [type=" + args[i].getClass().getName() + "] [value=" + args[i] + "]" : diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/MessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/MessageCondition.java index e2547f08f778..9fb9b81e88d6 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/MessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/MessageCondition.java @@ -16,7 +16,8 @@ package org.springframework.messaging.handler; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; /** @@ -47,8 +48,7 @@ public interface MessageCondition { * condition with sorted, matching patterns only. * @return a condition instance in case of a match; or {@code null} if there is no match. */ - @Nullable - T getMatchingCondition(Message message); + @Nullable T getMatchingCondition(Message message); /** * Compare this condition to another in the context of a specific message. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java index 21216f1bf6d2..9d5eb345b880 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/MessagingAdviceBean.java @@ -16,8 +16,9 @@ package org.springframework.messaging.handler; +import org.jspecify.annotations.Nullable; + import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; /** * Represents a Spring-managed bean with cross-cutting functionality to be @@ -40,8 +41,7 @@ public interface MessagingAdviceBean extends Ordered { *

    If the bean type is a CGLIB-generated class, the original user-defined * class is returned. */ - @Nullable - Class getBeanType(); + @Nullable Class getBeanType(); /** * Return the advice bean instance, if necessary resolving a bean specified diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java index 94b592f067ba..a5ff0004bab8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessor.java @@ -22,12 +22,13 @@ import java.lang.reflect.Type; import java.security.Principal; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.BindingReflectionHintsRegistrar; import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.annotation.ReflectiveProcessor; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.support.MessageHeaderAccessor; @@ -114,8 +115,7 @@ protected void registerReturnValueHints(ReflectionHints hints, Method method) { this.bindingRegistrar.registerReflectionHints(hints, returnType.getGenericParameterType()); } - @Nullable - protected Type getMessageType(MethodParameter parameter) { + protected @Nullable Type getMessageType(MethodParameter parameter) { MethodParameter nestedParameter = parameter.nested(); return (nestedParameter.getNestedParameterType() == nestedParameter.getParameterType() ? null : nestedParameter.getNestedParameterType()); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/package-info.java index 3259288c04f6..07a9bff88d22 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotations and support classes for handling messages. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.handler.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java index 1a169e7294d9..22e8cb9d0329 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/AbstractNamedValueMethodArgumentResolver.java @@ -19,6 +19,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; @@ -26,7 +28,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.ValueConstants; import org.springframework.messaging.handler.invocation.reactive.SyncHandlerMethodArgumentResolver; @@ -55,11 +56,9 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements SyncHa private final ConversionService conversionService; - @Nullable - private final ConfigurableBeanFactory configurableBeanFactory; + private final @Nullable ConfigurableBeanFactory configurableBeanFactory; - @Nullable - private final BeanExpressionContext expressionContext; + private final @Nullable BeanExpressionContext expressionContext; private final Map namedValueInfoCache = new ConcurrentHashMap<>(256); @@ -81,8 +80,7 @@ protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionS @Override - @Nullable - public Object resolveArgumentValue(MethodParameter parameter, Message message) { + public @Nullable Object resolveArgumentValue(MethodParameter parameter, Message message) { NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); @@ -167,8 +165,7 @@ private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValu * Resolve the given annotation-specified value, * potentially containing placeholders and expressions. */ - @Nullable - private Object resolveEmbeddedValuesAndExpressions(String value) { + private @Nullable Object resolveEmbeddedValuesAndExpressions(String value) { if (this.configurableBeanFactory == null || this.expressionContext == null) { return value; } @@ -187,8 +184,7 @@ private Object resolveEmbeddedValuesAndExpressions(String value) { * @param name the name of the value being resolved * @return the resolved argument. May be {@code null} */ - @Nullable - protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message message, String name); + protected abstract @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, String name); /** * Invoked when a value is required, but {@link #resolveArgumentInternal} @@ -205,8 +201,7 @@ private Object resolveEmbeddedValuesAndExpressions(String value) { * Specifically for booleans method parameters, use {@link Boolean#FALSE}. * Also raise an ISE for primitive types. */ - @Nullable - private Object handleNullValue(String name, @Nullable Object value, Class paramType) { + private @Nullable Object handleNullValue(String name, @Nullable Object value, Class paramType) { if (value == null) { if (paramType == boolean.class) { return Boolean.FALSE; @@ -231,8 +226,7 @@ protected static class NamedValueInfo { private final boolean required; - @Nullable - private final String defaultValue; + private final @Nullable String defaultValue; protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { this.name = name; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/DestinationVariableMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/DestinationVariableMethodArgumentResolver.java index 6cd3f07f08cf..367241fbc1ab 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/DestinationVariableMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/DestinationVariableMethodArgumentResolver.java @@ -18,9 +18,10 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.MessageHeaders; @@ -59,9 +60,8 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { } @Override - @Nullable @SuppressWarnings("unchecked") - protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { + protected @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { MessageHeaders headers = message.getHeaders(); Map vars = (Map) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER); return vars != null ? vars.get(name) : null; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolver.java index 0c632ee931bf..35f752b11d83 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeaderMethodArgumentResolver.java @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.handler.annotation.Header; @@ -67,8 +67,7 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { } @Override - @Nullable - protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { + protected @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { Object headerValue = message.getHeaders().get(name); Object nativeHeaderValue = getNativeHeaderValue(message, name); @@ -84,8 +83,7 @@ protected Object resolveArgumentInternal(MethodParameter parameter, Message m return (headerValue != null ? headerValue : nativeHeaderValue); } - @Nullable - private Object getNativeHeaderValue(Message message, String name) { + private @Nullable Object getNativeHeaderValue(Message message, String name) { Map> nativeHeaders = getNativeHeaders(message); if (name.startsWith("nativeHeaders.")) { name = name.substring("nativeHeaders.".length()); @@ -98,8 +96,7 @@ private Object getNativeHeaderValue(Message message, String name) { } @SuppressWarnings("unchecked") - @Nullable - private Map> getNativeHeaders(Message message) { + private @Nullable Map> getNativeHeaders(Message message) { return (Map>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeadersMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeadersMethodArgumentResolver.java index 3912b97ce22e..86cff187003f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeadersMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/HeadersMethodArgumentResolver.java @@ -19,8 +19,9 @@ import java.lang.reflect.Method; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.Headers; @@ -49,8 +50,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - @Nullable - public Object resolveArgumentValue(MethodParameter parameter, Message message) { + public @Nullable Object resolveArgumentValue(MethodParameter parameter, Message message) { Class paramType = parameter.getParameterType(); if (Map.class.isAssignableFrom(paramType)) { return message.getHeaders(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java index 850fefbf1759..6a910678fe68 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/MessageMappingMessageHandler.java @@ -28,6 +28,7 @@ import java.util.Set; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.beans.factory.config.ConfigurableBeanFactory; @@ -39,7 +40,6 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.CompositeMessageCondition; import org.springframework.messaging.handler.DestinationPatternsMessageCondition; @@ -86,16 +86,13 @@ public class MessageMappingMessageHandler extends AbstractMethodMessageHandler> decoders = new ArrayList<>(); - @Nullable - private Validator validator; + private @Nullable Validator validator; - @Nullable - private RouteMatcher routeMatcher; + private @Nullable RouteMatcher routeMatcher; private ConversionService conversionService = new DefaultFormattingConversionService(); - @Nullable - private StringValueResolver valueResolver; + private @Nullable StringValueResolver valueResolver; public MessageMappingMessageHandler() { @@ -130,8 +127,7 @@ public void setValidator(@Nullable Validator validator) { /** * Return the configured Validator instance. */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return this.validator; } @@ -151,8 +147,7 @@ public void setRouteMatcher(@Nullable RouteMatcher routeMatcher) { * Return the {@code RouteMatcher} used to map messages to handlers. * May be {@code null} before the component is initialized. */ - @Nullable - public RouteMatcher getRouteMatcher() { + public @Nullable RouteMatcher getRouteMatcher() { return this.routeMatcher; } @@ -272,8 +267,7 @@ protected List initReturnValueHandler @Override - @Nullable - protected CompositeMessageCondition getMappingForMethod(Method method, Class handlerType) { + protected @Nullable CompositeMessageCondition getMappingForMethod(Method method, Class handlerType) { CompositeMessageCondition methodCondition = getCondition(method); if (methodCondition != null) { CompositeMessageCondition typeCondition = getCondition(handlerType); @@ -289,8 +283,7 @@ protected CompositeMessageCondition getMappingForMethod(Method method, Class * @param element the element to check * @return the condition, or {@code null} */ - @Nullable - protected CompositeMessageCondition getCondition(AnnotatedElement element) { + protected @Nullable CompositeMessageCondition getCondition(AnnotatedElement element) { MessageMapping ann = AnnotatedElementUtils.findMergedAnnotation(element, MessageMapping.class); if (ann == null || ann.value().length == 0) { return null; @@ -326,15 +319,13 @@ protected Set getDirectLookupMappings(CompositeMessageCondition mapping) } @Override - @Nullable - protected RouteMatcher.Route getDestination(Message message) { + protected RouteMatcher.@Nullable Route getDestination(Message message) { return (RouteMatcher.Route) message.getHeaders() .get(DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER); } @Override - @Nullable - protected CompositeMessageCondition getMatchingMapping(CompositeMessageCondition mapping, Message message) { + protected @Nullable CompositeMessageCondition getMatchingMapping(CompositeMessageCondition mapping, Message message) { return mapping.getMatchingCondition(message); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java index 53004446d1cd..dbf352a148c9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolver.java @@ -24,6 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -37,7 +38,6 @@ import org.springframework.core.codec.DecodingException; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.Payload; @@ -79,8 +79,7 @@ public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResol private final List> decoders; - @Nullable - private final Validator validator; + private final @Nullable Validator validator; private final ReactiveAdapterRegistry adapterRegistry; @@ -108,8 +107,7 @@ public List> getDecoders() { /** * Return the configured validator, if any. */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return this.validator; } @@ -196,8 +194,7 @@ private MethodArgumentResolutionException getUnexpectedPayloadError( * {@link MimeType} value or a String to parse to a {@link MimeType}. * @param message the input message */ - @Nullable - protected MimeType getMimeType(Message message) { + protected @Nullable MimeType getMimeType(Message message) { Object headerValue = message.getHeaders().get(MessageHeaders.CONTENT_TYPE); if (headerValue == null) { return null; @@ -279,8 +276,7 @@ private MethodArgumentResolutionException handleMissingBody(MethodParameter para "Payload content is missing: " + param.getExecutable().toGenericString()); } - @Nullable - private Consumer getValidator(Message message, MethodParameter parameter) { + private @Nullable Consumer getValidator(Message message, MethodParameter parameter) { if (this.validator == null) { return null; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/package-info.java index cd1dcb8aba17..923889f9d850 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/reactive/package-info.java @@ -2,9 +2,7 @@ * Support classes for working with annotated message-handling methods with * non-blocking, reactive contracts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.handler.annotation.reactive; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java index fd17f5e1f7ec..2e8cab2909d8 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/AbstractNamedValueMethodArgumentResolver.java @@ -19,6 +19,8 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.config.BeanExpressionContext; import org.springframework.beans.factory.config.BeanExpressionResolver; @@ -27,7 +29,6 @@ import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.TypeDescriptor; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.annotation.ValueConstants; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; @@ -57,11 +58,9 @@ public abstract class AbstractNamedValueMethodArgumentResolver implements Handle private final ConversionService conversionService; - @Nullable - private final ConfigurableBeanFactory configurableBeanFactory; + private final @Nullable ConfigurableBeanFactory configurableBeanFactory; - @Nullable - private final BeanExpressionContext expressionContext; + private final @Nullable BeanExpressionContext expressionContext; private final Map namedValueInfoCache = new ConcurrentHashMap<>(256); @@ -89,8 +88,7 @@ protected AbstractNamedValueMethodArgumentResolver(ConversionService conversionS @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { NamedValueInfo namedValueInfo = getNamedValueInfo(parameter); MethodParameter nestedParameter = parameter.nestedIfOptional(); @@ -177,8 +175,7 @@ private NamedValueInfo updateNamedValueInfo(MethodParameter parameter, NamedValu * Resolve the given annotation-specified value, * potentially containing placeholders and expressions. */ - @Nullable - private Object resolveEmbeddedValuesAndExpressions(String value) { + private @Nullable Object resolveEmbeddedValuesAndExpressions(String value) { if (this.configurableBeanFactory == null || this.expressionContext == null) { return value; } @@ -198,8 +195,7 @@ private Object resolveEmbeddedValuesAndExpressions(String value) { * @return the resolved argument. May be {@code null} * @throws Exception in case of errors */ - @Nullable - protected abstract Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) + protected abstract @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) throws Exception; /** @@ -217,8 +213,7 @@ protected abstract Object resolveArgumentInternal(MethodParameter parameter, Mes * Specifically for booleans method parameters, use {@link Boolean#FALSE}. * Also raise an ISE for primitive types. */ - @Nullable - private Object handleNullValue(String name, @Nullable Object value, Class paramType) { + private @Nullable Object handleNullValue(String name, @Nullable Object value, Class paramType) { if (value == null) { if (paramType == boolean.class) { return Boolean.FALSE; @@ -254,8 +249,7 @@ protected static class NamedValueInfo { private final boolean required; - @Nullable - private final String defaultValue; + private final @Nullable String defaultValue; protected NamedValueInfo(String name, boolean required, @Nullable String defaultValue) { this.name = name; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java index b1022ff357d8..4637549f67a5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DefaultMessageHandlerMethodFactory.java @@ -20,13 +20,14 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.converter.GenericMessageConverter; import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; @@ -62,20 +63,16 @@ public class DefaultMessageHandlerMethodFactory private ConversionService conversionService = new DefaultFormattingConversionService(); - @Nullable - private MessageConverter messageConverter; + private @Nullable MessageConverter messageConverter; - @Nullable - private Validator validator; + private @Nullable Validator validator; - @Nullable - private List customArgumentResolvers; + private @Nullable List customArgumentResolvers; private final HandlerMethodArgumentResolverComposite argumentResolvers = new HandlerMethodArgumentResolverComposite(); - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java index ad80d5922dc1..7caab83952e5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/DestinationVariableMethodArgumentResolver.java @@ -18,9 +18,10 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.MessageHeaders; @@ -59,9 +60,8 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { } @Override - @Nullable @SuppressWarnings("unchecked") - protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { + protected @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) { MessageHeaders headers = message.getHeaders(); Map vars = (Map) headers.get(DESTINATION_TEMPLATE_VARIABLES_HEADER); return vars != null ? vars.get(name) : null; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java index e6b50f058631..e5b120ce5572 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeaderMethodArgumentResolver.java @@ -21,11 +21,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.core.MethodParameter; import org.springframework.core.convert.ConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandlingException; import org.springframework.messaging.handler.annotation.Header; @@ -68,8 +68,7 @@ protected NamedValueInfo createNamedValueInfo(MethodParameter parameter) { } @Override - @Nullable - protected Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) + protected @Nullable Object resolveArgumentInternal(MethodParameter parameter, Message message, String name) throws Exception { Object headerValue = message.getHeaders().get(name); @@ -86,8 +85,7 @@ protected Object resolveArgumentInternal(MethodParameter parameter, Message m return (headerValue != null ? headerValue : nativeHeaderValue); } - @Nullable - private Object getNativeHeaderValue(Message message, String name) { + private @Nullable Object getNativeHeaderValue(Message message, String name) { Map> nativeHeaders = getNativeHeaders(message); if (name.startsWith("nativeHeaders.")) { name = name.substring("nativeHeaders.".length()); @@ -100,8 +98,7 @@ private Object getNativeHeaderValue(Message message, String name) { } @SuppressWarnings("unchecked") - @Nullable - private Map> getNativeHeaders(Message message) { + private @Nullable Map> getNativeHeaders(Message message) { return (Map>) message.getHeaders().get(NativeMessageHeaderAccessor.NATIVE_HEADERS); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java index 1981c1b254d5..b4d3cce6bdfd 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/HeadersMethodArgumentResolver.java @@ -19,8 +19,9 @@ import java.lang.reflect.Method; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.Headers; @@ -49,8 +50,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { Class paramType = parameter.getParameterType(); if (Map.class.isAssignableFrom(paramType)) { return message.getHeaders(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java index 46f5c77884f1..4cac2c3a0e04 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MessageMethodArgumentResolver.java @@ -18,9 +18,10 @@ import java.lang.reflect.Type; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.converter.MessageConverter; @@ -43,8 +44,7 @@ */ public class MessageMethodArgumentResolver implements HandlerMethodArgumentResolver { - @Nullable - private final MessageConverter converter; + private final @Nullable MessageConverter converter; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentNotValidException.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentNotValidException.java index 9ab9da75ed6f..c581f86a6946 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentNotValidException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/MethodArgumentNotValidException.java @@ -16,8 +16,9 @@ package org.springframework.messaging.handler.annotation.support; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; import org.springframework.validation.BindingResult; @@ -34,8 +35,7 @@ @SuppressWarnings("serial") public class MethodArgumentNotValidException extends MethodArgumentResolutionException { - @Nullable - private final BindingResult bindingResult; + private final @Nullable BindingResult bindingResult; /** @@ -60,8 +60,7 @@ public MethodArgumentNotValidException(Message message, MethodParameter param * Return the BindingResult if the failure is validation-related, * or {@code null} if none. */ - @Nullable - public final BindingResult getBindingResult() { + public final @Nullable BindingResult getBindingResult() { return this.bindingResult; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java index e38629fbeeae..b6bd7c14c816 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/PayloadMethodArgumentResolver.java @@ -19,8 +19,9 @@ import java.lang.annotation.Annotation; import java.util.Optional; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConversionException; import org.springframework.messaging.converter.MessageConverter; @@ -61,8 +62,7 @@ public class PayloadMethodArgumentResolver implements HandlerMethodArgumentResol private final MessageConverter converter; - @Nullable - private final Validator validator; + private final @Nullable Validator validator; private final boolean useDefaultResolution; @@ -111,8 +111,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { Payload ann = parameter.getParameterAnnotation(Payload.class); if (ann != null && StringUtils.hasText(ann.expression())) { throw new IllegalStateException("@Payload SpEL expressions not supported by this resolver"); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/package-info.java index 494657e5c675..43dd5f21e721 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/annotation/support/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for working with annotated message-handling methods. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.handler.annotation.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java index 38801e420d80..85279806e1f1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractAsyncReturnValueHandler.java @@ -16,8 +16,9 @@ package org.springframework.messaging.handler.invocation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractExceptionHandlerMethodResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractExceptionHandlerMethodResolver.java index 1c5b1593c847..dfd7ea3342b1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractExceptionHandlerMethodResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractExceptionHandlerMethodResolver.java @@ -22,8 +22,9 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ExceptionDepthComparator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -98,8 +99,7 @@ public boolean hasExceptionMappings() { * @param exception the exception * @return a Method to handle the exception, or {@code null} if none found */ - @Nullable - public Method resolveMethod(Throwable exception) { + public @Nullable Method resolveMethod(Throwable exception) { Method method = resolveMethodByExceptionType(exception.getClass()); if (method == null) { Throwable cause = exception.getCause(); @@ -118,8 +118,7 @@ public Method resolveMethod(Throwable exception) { * @return a Method to handle the exception, or {@code null} if none found * @since 4.3.1 */ - @Nullable - public Method resolveMethodByExceptionType(Class exceptionType) { + public @Nullable Method resolveMethodByExceptionType(Class exceptionType) { Method method = this.exceptionLookupCache.get(exceptionType); if (method == null) { method = getMappedMethod(exceptionType); @@ -132,8 +131,7 @@ public Method resolveMethodByExceptionType(Class exceptionT * Return the {@link Method} mapped to the given exception type, or * {@link #NO_MATCHING_EXCEPTION_HANDLER_METHOD} if none. */ - @Nullable - private Method getMappedMethod(Class exceptionType) { + private @Nullable Method getMappedMethod(Class exceptionType) { List> matches = new ArrayList<>(); for (Class mappedException : this.mappedMethods.keySet()) { if (mappedException.isAssignableFrom(exceptionType)) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java index c469fe0db2d5..ee92516c4873 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AbstractMethodMessageHandler.java @@ -34,13 +34,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.core.MethodIntrospector; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessageHandlingException; @@ -89,8 +89,7 @@ public abstract class AbstractMethodMessageHandler protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Log handlerMethodLogger; + private @Nullable Log handlerMethodLogger; private final List destinationPrefixes = new ArrayList<>(); @@ -105,8 +104,7 @@ public abstract class AbstractMethodMessageHandler private final HandlerMethodReturnValueHandlerComposite returnValueHandlers = new HandlerMethodReturnValueHandlerComposite(); - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; private final Map handlerMethods = new LinkedHashMap<>(64); @@ -225,8 +223,7 @@ public void setApplicationContext(@Nullable ApplicationContext applicationContex this.applicationContext = applicationContext; } - @Nullable - public ApplicationContext getApplicationContext() { + public @Nullable ApplicationContext getApplicationContext() { return this.applicationContext; } @@ -343,8 +340,7 @@ private String formatMappings(Class userType, Map methods) { * @param handlerType the handler type, possibly a subtype of the method's declaring class * @return the mapping, or {@code null} if the method is not mapped */ - @Nullable - protected abstract T getMappingForMethod(Method method, Class handlerType); + protected abstract @Nullable T getMappingForMethod(Method method, Class handlerType); /** * Register a handler method and its unique mapping. @@ -399,8 +395,7 @@ protected HandlerMethod createHandlerMethod(Object handler, Method method) { * Return a logger to set on {@link HandlerMethodReturnValueHandlerComposite}. * @since 5.1 */ - @Nullable - protected Log getReturnValueHandlerLogger() { + protected @Nullable Log getReturnValueHandlerLogger() { return null; } @@ -408,8 +403,7 @@ protected Log getReturnValueHandlerLogger() { * Return a logger to set on {@link InvocableHandlerMethod}. * @since 5.1 */ - @Nullable - protected Log getHandlerMethodLogger() { + protected @Nullable Log getHandlerMethodLogger() { return null; } @@ -458,8 +452,7 @@ public void handleMessage(Message message) throws MessagingException { headerAccessor.setImmutable(); } - @Nullable - protected abstract String getDestination(Message message); + protected abstract @Nullable String getDestination(Message message); /** * Check whether the given destination (of an incoming message) matches to @@ -469,8 +462,7 @@ public void handleMessage(Message message) throws MessagingException { *

    If there are no destination prefixes, return the destination as is. */ @SuppressWarnings("ForLoopReplaceableByForEach") - @Nullable - protected String getLookupDestination(@Nullable String destination) { + protected @Nullable String getLookupDestination(@Nullable String destination) { if (destination == null) { return null; } @@ -522,12 +514,13 @@ protected void handleMessageInternal(Message message, String lookupDestinatio handleMatch(bestMatch.mapping, bestMatch.handlerMethod, lookupDestination, message); } - @SuppressWarnings("NullAway") private void addMatchesToCollection(Collection mappingsToCheck, Message message, List matches) { for (T mapping : mappingsToCheck) { T match = getMatchingMapping(mapping, message); if (match != null) { - matches.add(new Match(match, this.handlerMethods.get(mapping))); + HandlerMethod handlerMethod = this.handlerMethods.get(mapping); + Assert.state(handlerMethod != null, "HandlerMethod must not be null"); + matches.add(new Match(match, handlerMethod)); } } } @@ -539,8 +532,7 @@ private void addMatchesToCollection(Collection mappingsToCheck, Message me * @param message the message being handled * @return the match or {@code null} if there is no match */ - @Nullable - protected abstract T getMatchingMapping(T mapping, Message message); + protected abstract @Nullable T getMatchingMapping(T mapping, Message message); protected void handleNoMatch(Set ts, String lookupDestination, Message message) { logger.debug("No matching message handler methods."); @@ -628,8 +620,7 @@ protected void processHandlerMethodException(HandlerMethod handlerMethod, Except * @return a method to handle the exception, or {@code null} * @since 4.2 */ - @Nullable - protected InvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { + protected @Nullable InvocableHandlerMethod getExceptionHandlerMethod(HandlerMethod handlerMethod, Exception exception) { if (logger.isDebugEnabled()) { logger.debug("Searching methods to handle " + exception.getClass().getSimpleName()); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java index ce41c94c1fc2..cff9ff3fede1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/AsyncHandlerMethodReturnValueHandler.java @@ -18,8 +18,9 @@ import java.util.concurrent.CompletableFuture; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; /** * An extension of {@link HandlerMethodReturnValueHandler} for handling async, @@ -48,35 +49,6 @@ public interface AsyncHandlerMethodReturnValueHandler extends HandlerMethodRetur */ boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType); - /** - * Adapt the asynchronous return value to a - * {@link org.springframework.util.concurrent.ListenableFuture ListenableFuture}. - *

    Implementations should consider returning an instance of - * {@link org.springframework.util.concurrent.SettableListenableFuture - * SettableListenableFuture}. Return value handling will then continue when - * the ListenableFuture is completed with either success or error. - *

    Note: this method will only be invoked after - * {@link #supportsReturnType(org.springframework.core.MethodParameter)} - * is called and it returns {@code true}. - * @param returnValue the value returned from the handler method - * @param returnType the type of the return value - * @return the resulting ListenableFuture, or {@code null} in which case - * no further handling will be performed - * @deprecated as of 6.0, in favor of - * {@link #toCompletableFuture(Object, MethodParameter)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - @Nullable - default org.springframework.util.concurrent.ListenableFuture toListenableFuture( - Object returnValue, MethodParameter returnType) { - - CompletableFuture result = toCompletableFuture(returnValue, returnType); - return (result != null ? - new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>(result) : - null); - } - /** * Adapt the asynchronous return value to a {@link CompletableFuture}. *

    Return value handling will then continue when @@ -90,7 +62,6 @@ default org.springframework.util.concurrent.ListenableFuture toListenableFutu * no further handling will be performed * @since 6.0 */ - @Nullable - CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType); + @Nullable CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolver.java index 4d1c17f95d99..3a49369a181e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolver.java @@ -16,8 +16,9 @@ package org.springframework.messaging.handler.invocation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -48,7 +49,6 @@ public interface HandlerMethodArgumentResolver { * @return the resolved argument value, or {@code null} * @throws Exception in case of errors with the preparation of argument values */ - @Nullable - Object resolveArgument(MethodParameter parameter, Message message) throws Exception; + @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolverComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolverComposite.java index 966fa7083496..5b89bb8cda59 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolverComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodArgumentResolverComposite.java @@ -22,8 +22,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -108,8 +109,7 @@ public boolean supportsParameter(MethodParameter parameter) { * @throws IllegalArgumentException if no suitable argument resolver is found */ @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message) throws Exception { + public @Nullable Object resolveArgument(MethodParameter parameter, Message message) throws Exception { HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter); if (resolver == null) { throw new IllegalArgumentException("Unsupported parameter type [" + @@ -122,8 +122,7 @@ public Object resolveArgument(MethodParameter parameter, Message message) thr * Find a registered {@link HandlerMethodArgumentResolver} that supports * the given method parameter. */ - @Nullable - private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + private @Nullable HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandler.java index 3dca2443cc4c..ca768b7d7f89 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandler.java @@ -16,8 +16,9 @@ package org.springframework.messaging.handler.invocation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java index 5e3ed7e3a8a0..d2b43df03157 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/HandlerMethodReturnValueHandlerComposite.java @@ -23,9 +23,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -102,8 +102,7 @@ public boolean supportsReturnType(MethodParameter returnType) { } @SuppressWarnings("ForLoopReplaceableByForEach") - @Nullable - private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { + private @Nullable HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { if (handler.supportsReturnType(returnType)) { return handler; @@ -134,8 +133,7 @@ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType } @Override - @Nullable - public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { + public @Nullable CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { HandlerMethodReturnValueHandler handler = getReturnValueHandler(returnType); if (handler instanceof AsyncHandlerMethodReturnValueHandler asyncHandler) { return asyncHandler.toCompletableFuture(returnValue, returnType); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java index cc74ad6ed900..c3a6ce5609af 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethod.java @@ -21,11 +21,12 @@ import java.lang.reflect.Type; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.MethodParameter; import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.util.ObjectUtils; @@ -110,9 +111,8 @@ public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDisc * @see #getMethodArgumentValues * @see #doInvoke */ - @Nullable - public Object invoke(Message message, @Nullable Object... providedArgs) throws Exception { - Object[] args = getMethodArgumentValues(message, providedArgs); + public @Nullable Object invoke(Message message, @Nullable Object... providedArgs) throws Exception { + @Nullable Object[] args = getMethodArgumentValues(message, providedArgs); if (logger.isTraceEnabled()) { logger.trace("Arguments: " + Arrays.toString(args)); } @@ -125,13 +125,13 @@ public Object invoke(Message message, @Nullable Object... providedArgs) throw *

    The resulting array will be passed into {@link #doInvoke}. * @since 5.1.2 */ - protected Object[] getMethodArgumentValues(Message message, @Nullable Object... providedArgs) throws Exception { + protected @Nullable Object[] getMethodArgumentValues(Message message, @Nullable Object... providedArgs) throws Exception { MethodParameter[] parameters = getMethodParameters(); if (ObjectUtils.isEmpty(parameters)) { return EMPTY_ARGS; } - Object[] args = new Object[parameters.length]; + @Nullable Object[] args = new Object[parameters.length]; for (int i = 0; i < parameters.length; i++) { MethodParameter parameter = parameters[i]; parameter.initParameterNameDiscovery(this.parameterNameDiscoverer); @@ -163,8 +163,7 @@ protected Object[] getMethodArgumentValues(Message message, @Nullable Object. /** * Invoke the handler method with the given argument values. */ - @Nullable - protected Object doInvoke(Object... args) throws Exception { + protected @Nullable Object doInvoke(@Nullable Object... args) throws Exception { try { return getBridgedMethod().invoke(getBean(), args); } @@ -199,8 +198,7 @@ MethodParameter getAsyncReturnValueType(@Nullable Object returnValue) { private class AsyncResultMethodParameter extends AnnotatedMethodParameter { - @Nullable - private final Object returnValue; + private final @Nullable Object returnValue; private final ResolvableType returnType; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java deleted file mode 100644 index 93aeb5952cd8..000000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ListenableFutureReturnValueHandler.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2002-2024 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.messaging.handler.invocation; - -import java.util.concurrent.CompletableFuture; - -import org.springframework.core.MethodParameter; - -/** - * Support for {@link org.springframework.util.concurrent.ListenableFuture} as a return value type. - * - * @author Sebastien Deleuze - * @since 4.2 - * @deprecated as of 6.0, in favor of {@link CompletableFutureReturnValueHandler} - */ -@Deprecated(since = "6.0", forRemoval = true) -@SuppressWarnings("removal") -public class ListenableFutureReturnValueHandler extends AbstractAsyncReturnValueHandler { - - @Override - public boolean supportsReturnType(MethodParameter returnType) { - return org.springframework.util.concurrent.ListenableFuture.class.isAssignableFrom(returnType.getParameterType()); - } - - @Override - public org.springframework.util.concurrent.ListenableFuture toListenableFuture(Object returnValue, MethodParameter returnType) { - return (org.springframework.util.concurrent.ListenableFuture) returnValue; - } - - @Override - public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { - return ((org.springframework.util.concurrent.ListenableFuture) returnValue).completable(); - } - -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/MethodArgumentResolutionException.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/MethodArgumentResolutionException.java index 21a795a807eb..1e4f6e1e156b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/MethodArgumentResolutionException.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/MethodArgumentResolutionException.java @@ -16,8 +16,9 @@ package org.springframework.messaging.handler.invocation; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java index 18a5948f64d2..73cf3412dbc3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/ReactiveReturnValueHandler.java @@ -18,12 +18,12 @@ import java.util.concurrent.CompletableFuture; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; /** * Support for single-value reactive types (like {@code Mono} or {@code Single}) @@ -58,8 +58,7 @@ public boolean isAsyncReturnValue(Object returnValue, MethodParameter returnType } @Override - @Nullable - public CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { + public @Nullable CompletableFuture toCompletableFuture(Object returnValue, MethodParameter returnType) { ReactiveAdapter adapter = this.adapterRegistry.getAdapter(returnType.getParameterType(), returnValue); if (adapter != null) { return Mono.from(adapter.toPublisher(returnValue)).toFuture(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/package-info.java index eb05eec33d8e..1fa831548820 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/package-info.java @@ -1,9 +1,7 @@ /** * Common infrastructure for invoking message handler methods. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.handler.invocation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractEncoderMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractEncoderMethodReturnValueHandler.java index bfb05d91ed84..5d5d5198aa18 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractEncoderMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractEncoderMethodReturnValueHandler.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -36,7 +37,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.MessagingException; @@ -166,9 +166,8 @@ else if (type != ResolvableType.NONE) { } } - @Nullable @SuppressWarnings("unchecked") - private Encoder getEncoder(ResolvableType elementType, @Nullable MimeType mimeType) { + private @Nullable Encoder getEncoder(ResolvableType elementType, @Nullable MimeType mimeType) { for (Encoder encoder : getEncoders()) { if (encoder.canEncode(elementType, mimeType)) { return (Encoder) encoder; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java index c8731bfc49d2..df813633e692 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/AbstractMethodMessageHandler.java @@ -33,6 +33,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.beans.factory.BeanNameAware; @@ -41,7 +42,6 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.core.MethodIntrospector; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessagingException; import org.springframework.messaging.ReactiveMessageHandler; @@ -87,11 +87,9 @@ public abstract class AbstractMethodMessageHandler protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private Predicate> handlerPredicate; + private @Nullable Predicate> handlerPredicate; - @Nullable - List handlers; + @Nullable List handlers; private ArgumentResolverConfigurer argumentResolverConfigurer = new ArgumentResolverConfigurer(); @@ -99,11 +97,9 @@ public abstract class AbstractMethodMessageHandler private final InvocableHelper invocableHelper = new InvocableHelper(this::createExceptionMethodResolverFor); - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private String beanName; + private @Nullable String beanName; private final Map handlerMethods = new ConcurrentHashMap<>(64); @@ -124,8 +120,7 @@ public void setHandlerPredicate(@Nullable Predicate> handlerPredicate) /** * Return the {@link #setHandlerPredicate configured} handler predicate. */ - @Nullable - public Predicate> getHandlerPredicate() { + public @Nullable Predicate> getHandlerPredicate() { return this.handlerPredicate; } @@ -193,8 +188,7 @@ public void setApplicationContext(@Nullable ApplicationContext applicationContex this.applicationContext = applicationContext; } - @Nullable - public ApplicationContext getApplicationContext() { + public @Nullable ApplicationContext getApplicationContext() { return this.applicationContext; } @@ -365,8 +359,7 @@ private String formatMappings(Class userType, Map methods) { * @param handlerType the handler type, possibly a subtype of the method's declaring class * @return the mapping, or {@code null} if the method is not mapped */ - @Nullable - protected abstract T getMappingForMethod(Method method, Class handlerType); + protected abstract @Nullable T getMappingForMethod(Method method, Class handlerType); /** * Register a handler method and its unique mapping. @@ -461,8 +454,7 @@ protected Mono handleMatch(T mapping, HandlerMethod handlerMethod, Message return this.invocableHelper.handleMessage(handlerMethod, message); } - @Nullable - private Match getHandlerMethod(Message message) { + private @Nullable Match getHandlerMethod(Message message) { List> matches = new ArrayList<>(); RouteMatcher.Route destination = getDestination(message); @@ -502,17 +494,17 @@ private Match getHandlerMethod(Message message) { * Extract the destination from the given message. * @see #getDirectLookupMappings(Object) */ - @Nullable - protected abstract RouteMatcher.Route getDestination(Message message); + protected abstract RouteMatcher.@Nullable Route getDestination(Message message); - @SuppressWarnings("NullAway") private void addMatchesToCollection( Collection mappingsToCheck, Message message, List> matches) { for (T mapping : mappingsToCheck) { T match = getMatchingMapping(mapping, message); if (match != null) { - matches.add(new Match<>(match, this.handlerMethods.get(mapping))); + HandlerMethod handlerMethod = this.handlerMethods.get(mapping); + Assert.state(handlerMethod != null, "HandlerMethod must not be null"); + matches.add(new Match<>(match, handlerMethod)); } } } @@ -524,8 +516,7 @@ private void addMatchesToCollection( * @param message the message being handled * @return the match or {@code null} if there is no match */ - @Nullable - protected abstract T getMatchingMapping(T mapping, Message message); + protected abstract @Nullable T getMatchingMapping(T mapping, Message message); /** * Return a comparator for sorting matching mappings. @@ -540,7 +531,7 @@ private void addMatchesToCollection( * @param destination the destination * @param message the message */ - protected void handleNoMatch(@Nullable RouteMatcher.Route destination, Message message) { + protected void handleNoMatch(RouteMatcher.@Nullable Route destination, Message message) { logger.debug("No handlers for destination '" + (destination != null ? destination.value() : "") + "'"); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java index 0edef96cf256..2c77297a77f6 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/ChannelSendOperator.java @@ -18,6 +18,7 @@ import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.reactivestreams.Subscriber; import org.reactivestreams.Subscription; @@ -30,7 +31,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -68,9 +68,8 @@ public ChannelSendOperator(Publisher source, Function, @Override - @Nullable @SuppressWarnings("rawtypes") - public Object scanUnsafe(Attr key) { + public @Nullable Object scanUnsafe(Attr key) { if (key == Attr.PREFETCH) { return Integer.MAX_VALUE; } @@ -134,16 +133,13 @@ private class WriteBarrier implements CoreSubscriber, Subscription, Publisher private final WriteCompletionBarrier writeCompletionBarrier; /* Upstream write source subscription */ - @Nullable - private Subscription subscription; + private @Nullable Subscription subscription; /** Cached data item before readyToWrite. */ - @Nullable - private T item; + private @Nullable T item; /** Cached error signal before readyToWrite. */ - @Nullable - private Throwable error; + private @Nullable Throwable error; /** Cached onComplete signal before readyToWrite. */ private boolean completed = false; @@ -155,8 +151,7 @@ private class WriteBarrier implements CoreSubscriber, Subscription, Publisher private State state = State.NEW; /** The actual writeSubscriber from the HTTP server adapter. */ - @Nullable - private Subscriber writeSubscriber; + private @Nullable Subscriber writeSubscriber; WriteBarrier(CoreSubscriber completionSubscriber) { @@ -391,8 +386,7 @@ private class WriteCompletionBarrier implements CoreSubscriber, Subscripti private final WriteBarrier writeBarrier; - @Nullable - private Subscription subscription; + private @Nullable Subscription subscription; public WriteCompletionBarrier(CoreSubscriber subscriber, WriteBarrier writeBarrier) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodArgumentResolverComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodArgumentResolverComposite.java index fc73bf63dea7..5ea4d42cde2a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodArgumentResolverComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodArgumentResolverComposite.java @@ -24,10 +24,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -124,8 +124,7 @@ public Mono resolveArgument(MethodParameter parameter, Message messag * Find a registered {@link HandlerMethodArgumentResolver} that supports * the given method parameter. */ - @Nullable - public HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { + public @Nullable HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) { HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter); if (result == null) { for (HandlerMethodArgumentResolver methodArgumentResolver : this.argumentResolvers) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandler.java index 4d0efbdeb6b8..7628048e1484 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandler.java @@ -16,10 +16,10 @@ package org.springframework.messaging.handler.invocation.reactive; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandlerComposite.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandlerComposite.java index 724fc8b650d0..ebe91fee7ee9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandlerComposite.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/HandlerMethodReturnValueHandlerComposite.java @@ -22,10 +22,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -93,8 +93,7 @@ public Mono handleReturnValue(@Nullable Object returnValue, MethodParamete } @SuppressWarnings("ForLoopReplaceableByForEach") - @Nullable - private HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { + private @Nullable HandlerMethodReturnValueHandler getReturnValueHandler(MethodParameter returnType) { for (HandlerMethodReturnValueHandler handler : this.returnValueHandlers) { if (handler.supportsReturnType(returnType)) { return handler; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHelper.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHelper.java index ccb9c3c198b3..91a0cd9fe4af 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHelper.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/InvocableHelper.java @@ -25,11 +25,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.HandlerMethod; import org.springframework.messaging.handler.MessagingAdviceBean; @@ -146,8 +146,7 @@ public InvocableHandlerMethod initMessageMappingMethod(HandlerMethod handlerMeth * @param ex the exception raised or signaled * @return a method to handle the exception, or {@code null} */ - @Nullable - public InvocableHandlerMethod initExceptionHandlerMethod(HandlerMethod handlerMethod, Throwable ex) { + public @Nullable InvocableHandlerMethod initExceptionHandlerMethod(HandlerMethod handlerMethod, Throwable ex) { if (logger.isDebugEnabled()) { logger.debug("Searching for methods to handle " + ex.getClass().getSimpleName()); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java index e933104d5c40..8a4f05369769 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/SyncHandlerMethodArgumentResolver.java @@ -16,10 +16,10 @@ package org.springframework.messaging.handler.invocation.reactive; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -47,7 +47,6 @@ default Mono resolveArgument(MethodParameter parameter, Message messa * @param message the currently processed message * @return the resolved value, if any */ - @Nullable - Object resolveArgumentValue(MethodParameter parameter, Message message); + @Nullable Object resolveArgumentValue(MethodParameter parameter, Message message); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/package-info.java index 27a85c242db9..a4d42cb5fe7e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/invocation/reactive/package-info.java @@ -2,9 +2,7 @@ * Common infrastructure for invoking message handler methods with non-blocking, * and reactive contracts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.handler.invocation.reactive; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/handler/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/handler/package-info.java index d4ae3f87b9d2..9ca6dcb80926 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/handler/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/handler/package-info.java @@ -1,9 +1,7 @@ /** * Basic abstractions for working with message handler methods. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.handler; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/package-info.java index 875d7b5b727c..2d31f80a4e37 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/package-info.java @@ -1,9 +1,7 @@ /** * Support for working with messaging APIs and protocols. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java index 9db72199ddb2..d791cc8f5e67 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultMetadataExtractor.java @@ -32,13 +32,13 @@ import io.rsocket.metadata.WellKnownMimeType; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; import org.springframework.core.io.buffer.NettyDataBuffer; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequester.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequester.java index 43ea6d6f64b3..73117463fd38 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequester.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequester.java @@ -23,6 +23,7 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.core.RSocketClient; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -35,7 +36,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MimeType; @@ -52,8 +52,7 @@ final class DefaultRSocketRequester implements RSocketRequester { private final RSocketClient rsocketClient; - @Nullable - private final RSocket rsocket; + private final @Nullable RSocket rsocket; private final MimeType dataMimeType; @@ -87,9 +86,8 @@ public RSocketClient rsocketClient() { return this.rsocketClient; } - @Nullable @Override - public RSocket rsocket() { + public @Nullable RSocket rsocket() { return this.rsocket; } @@ -132,11 +130,9 @@ private class DefaultRequestSpec implements RequestSpec { private final MetadataEncoder metadataEncoder = new MetadataEncoder(metadataMimeType(), strategies); - @Nullable - private Mono payloadMono; + private @Nullable Mono payloadMono; - @Nullable - private Flux payloadFlux; + private @Nullable Flux payloadFlux; public DefaultRequestSpec(String route, Object... vars) { @@ -178,8 +174,7 @@ public RequestSpec data(Object producer, Class elementClass) { return this; } - @Nullable - private ReactiveAdapter getAdapter(Class aClass) { + private @Nullable ReactiveAdapter getAdapter(Class aClass) { return strategies.reactiveAdapterRegistry().getAdapter(aClass); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilder.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilder.java index e3a045d7f20a..fb2a63595c45 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketRequesterBuilder.java @@ -36,6 +36,7 @@ import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; import io.rsocket.util.DefaultPayload; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -47,7 +48,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; @@ -69,26 +69,19 @@ final class DefaultRSocketRequesterBuilder implements RSocketRequester.Builder { private static final Payload EMPTY_SETUP_PAYLOAD = DefaultPayload.create(EMPTY_BYTE_ARRAY); - @Nullable - private MimeType dataMimeType; + private @Nullable MimeType dataMimeType; - @Nullable - private MimeType metadataMimeType; + private @Nullable MimeType metadataMimeType; - @Nullable - private Object setupData; + private @Nullable Object setupData; - @Nullable - private String setupRoute; + private @Nullable String setupRoute; - @Nullable - private Object[] setupRouteVars; + private Object @Nullable [] setupRouteVars; - @Nullable - private Map setupMetadata; + private @Nullable Map setupMetadata; - @Nullable - private RSocketStrategies strategies; + private @Nullable RSocketStrategies strategies; private final List> strategiesConfigurers = new ArrayList<>(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java index ec6ceab06bc0..385d3036c3f9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/DefaultRSocketStrategies.java @@ -23,6 +23,7 @@ import java.util.function.Consumer; import io.netty.buffer.PooledByteBufAllocator; +import org.jspecify.annotations.Nullable; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.codec.ByteArrayDecoder; @@ -37,7 +38,6 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.RouteMatcher; import org.springframework.util.SimpleRouteMatcher; @@ -117,17 +117,13 @@ static class DefaultRSocketStrategiesBuilder implements RSocketStrategies.Builde private final List> decoders = new ArrayList<>(); - @Nullable - private RouteMatcher routeMatcher; + private @Nullable RouteMatcher routeMatcher; - @Nullable - private ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); + private @Nullable ReactiveAdapterRegistry adapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); - @Nullable - private DataBufferFactory bufferFactory; + private @Nullable DataBufferFactory bufferFactory; - @Nullable - private MetadataExtractor metadataExtractor; + private @Nullable MetadataExtractor metadataExtractor; private final List> metadataExtractors = new ArrayList<>(); 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 507745c7e127..5f153b3c39b2 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 @@ -27,6 +27,7 @@ import io.netty.buffer.ByteBufAllocator; import io.netty.buffer.CompositeByteBuf; import io.rsocket.metadata.WellKnownMimeType; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.ReactiveAdapter; @@ -35,7 +36,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.MimeType; @@ -63,8 +63,7 @@ final class MetadataEncoder { private final ByteBufAllocator allocator; - @Nullable - private String route; + private @Nullable String route; private final List metadataEntries = new ArrayList<>(4); @@ -155,7 +154,7 @@ else if (!this.metadataMimeType.equals(mimeType)) { * Add route and/or metadata, both optional. */ public MetadataEncoder metadataAndOrRoute(@Nullable Map metadata, - @Nullable String route, @Nullable Object[] vars) { + @Nullable String route, Object @Nullable [] vars) { if (route != null) { this.route = expand(route, vars != null ? vars : new Object[0]); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java index 667e02cd3022..4856cf398ca9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/MetadataExtractorRegistry.java @@ -19,8 +19,9 @@ import java.util.Map; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; /** 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 2c946d5c3137..108651f3ea96 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. @@ -29,6 +29,7 @@ import io.rsocket.transport.ClientTransport; import io.rsocket.transport.netty.client.TcpClientTransport; import io.rsocket.transport.netty.client.WebsocketClientTransport; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.Disposable; import reactor.core.publisher.Flux; @@ -37,7 +38,6 @@ import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.codec.Decoder; -import org.springframework.lang.Nullable; import org.springframework.messaging.rsocket.annotation.support.RSocketMessageHandler; import org.springframework.util.MimeType; @@ -64,8 +64,7 @@ public interface RSocketRequester extends Disposable { * or via one of the (deprecated) connect methods on the * {@code RSocketRequester} builder, or otherwise return {@code null}. */ - @Nullable - RSocket rsocket(); + @Nullable RSocket rsocket(); /** * Return the data {@code MimeType} selected for the underlying RSocket @@ -311,9 +310,9 @@ RSocketRequester transports( * @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); /** @@ -321,18 +320,18 @@ RSocketRequester transports( * @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/rsocket/RSocketStrategies.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java index 8e8a43a10f16..914deca8a18e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/RSocketStrategies.java @@ -20,6 +20,7 @@ import java.util.function.Consumer; import io.rsocket.Payload; +import org.jspecify.annotations.Nullable; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.ResolvableType; @@ -28,7 +29,6 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.buffer.NettyDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.util.MimeType; import org.springframework.util.RouteMatcher; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/package-info.java index ed54c26543bb..272bc6591bed 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotations and support classes for handling RSocket streams. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.rsocket.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java index b7686c46f5e4..163fc0a65181 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/MessagingRSocket.java @@ -24,6 +24,7 @@ import io.rsocket.Payload; import io.rsocket.RSocket; import io.rsocket.frame.FrameType; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -31,7 +32,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.NettyDataBuffer; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.ReactiveMessageHandler; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java index e8167a3f1402..111a8f6b8efb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageCondition.java @@ -24,8 +24,8 @@ import java.util.Set; import io.rsocket.frame.FrameType; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.AbstractMessageCondition; import org.springframework.util.Assert; @@ -117,8 +117,7 @@ protected String getToStringInfix() { * @param message the current message * @return the frame type or {@code null} if not found */ - @Nullable - public static FrameType getFrameType(Message message) { + public static @Nullable FrameType getFrameType(Message message) { return (FrameType) message.getHeaders().get(RSocketFrameTypeMessageCondition.FRAME_TYPE_HEADER); } @@ -134,8 +133,7 @@ public RSocketFrameTypeMessageCondition combine(RSocketFrameTypeMessageCondition } @Override - @Nullable - public RSocketFrameTypeMessageCondition getMatchingCondition(Message message) { + public @Nullable RSocketFrameTypeMessageCondition getMatchingCondition(Message message) { FrameType actual = message.getHeaders().get(FRAME_TYPE_HEADER, FrameType.class); if (actual != null) { for (FrameType type : this.frameTypes) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketMessageHandler.java index d83119b1b7ff..a663a8b0d1da 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketMessageHandler.java @@ -28,6 +28,7 @@ import io.rsocket.SocketAcceptor; import io.rsocket.frame.FrameType; import io.rsocket.metadata.WellKnownMimeType; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.beans.BeanUtils; @@ -38,7 +39,6 @@ import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.PooledDataBuffer; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.handler.CompositeMessageCondition; @@ -96,8 +96,7 @@ public class RSocketMessageHandler extends MessageMappingMessageHandler { private RSocketStrategies strategies = RSocketStrategies.create(); - @Nullable - private MimeType defaultDataMimeType; + private @Nullable MimeType defaultDataMimeType; private MimeType defaultMetadataMimeType = MimeTypeUtils.parseMimeType( WellKnownMimeType.MESSAGE_RSOCKET_COMPOSITE_METADATA.getString()); @@ -256,8 +255,7 @@ public void setDefaultDataMimeType(@Nullable MimeType mimeType) { * Return the configured * {@link #setDefaultDataMimeType defaultDataMimeType}, or {@code null}. */ - @Nullable - public MimeType getDefaultDataMimeType() { + public @Nullable MimeType getDefaultDataMimeType() { return this.defaultDataMimeType; } @@ -311,8 +309,7 @@ protected List initReturnValueHandler @Override - @Nullable - protected CompositeMessageCondition getCondition(AnnotatedElement element) { + protected @Nullable CompositeMessageCondition getCondition(AnnotatedElement element) { MessageMapping ann1 = AnnotatedElementUtils.findMergedAnnotation(element, MessageMapping.class); if (ann1 != null && ann1.value().length > 0) { return new CompositeMessageCondition( @@ -379,7 +376,7 @@ else if (parameter.nested().getNestedParameterType().equals(Void.class)) { } @Override - protected void handleNoMatch(@Nullable RouteMatcher.Route destination, Message message) { + protected void handleNoMatch(RouteMatcher.@Nullable Route destination, Message message) { FrameType frameType = RSocketFrameTypeMessageCondition.getFrameType(message); if (frameType == FrameType.SETUP || frameType == FrameType.METADATA_PUSH) { if (frameType == FrameType.SETUP && message.getPayload() instanceof PooledDataBuffer pooledDataBuffer) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketPayloadReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketPayloadReturnValueHandler.java index 78f8ce774b5b..dc86c82d7dfa 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketPayloadReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/RSocketPayloadReturnValueHandler.java @@ -20,6 +20,7 @@ import java.util.concurrent.atomic.AtomicReference; import io.rsocket.Payload; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,7 +28,6 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.codec.Encoder; import org.springframework.core.io.buffer.DataBuffer; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.reactive.AbstractEncoderMethodReturnValueHandler; import org.springframework.messaging.rsocket.PayloadUtils; @@ -75,9 +75,8 @@ protected Mono handleNoContent(MethodParameter returnType, Message mess return Mono.empty(); } - @Nullable @SuppressWarnings("unchecked") - private AtomicReference> getResponseReference(Message message) { + private @Nullable AtomicReference> getResponseReference(Message message) { Object headerValue = message.getHeaders().get(RESPONSE_HEADER); Assert.state(headerValue == null || headerValue instanceof AtomicReference, "Expected AtomicReference"); return (AtomicReference>) headerValue; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/package-info.java index 4c02159a3c0c..ee1caa3abef6 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/annotation/support/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for working with annotated RSocket stream handling methods. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.rsocket.annotation.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/package-info.java index 9cb5ed03acdf..5d33cca2d618 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/package-info.java @@ -1,9 +1,7 @@ /** * Support for the RSocket protocol. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.rsocket; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java index 99b00160da2a..82d98d91d2de 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/DestinationVariableArgumentResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -18,8 +18,9 @@ import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.DestinationVariable; /** @@ -48,8 +49,8 @@ public boolean resolve( collection.forEach(requestValues::addRouteVariable); return true; } - else if (argument.getClass().isArray()) { - for (Object variable : (Object[]) argument) { + else if (argument instanceof Object[] arguments) { + for (Object variable : arguments) { requestValues.addRouteVariable(variable); } return true; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/MetadataArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/MetadataArgumentResolver.java index 3e89f95bede2..537fca18953e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/MetadataArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/MetadataArgumentResolver.java @@ -16,8 +16,9 @@ package org.springframework.messaging.rsocket.service; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolver.java index b80c747bbdc2..bcb140458246 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolver.java @@ -16,11 +16,12 @@ package org.springframework.messaging.rsocket.service; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ReactiveAdapter; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.Payload; import org.springframework.util.Assert; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java index d9ed6be31461..f80a6743ad9a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessor.java @@ -19,6 +19,8 @@ import java.util.HashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.framework.AopProxyUtils; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.ProxyHints; @@ -28,7 +30,6 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.Search; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -46,9 +47,8 @@ */ class RSocketExchangeBeanRegistrationAotProcessor implements BeanRegistrationAotProcessor { - @Nullable @Override - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); Set> exchangeInterfaces = new HashSet<>(); Search search = MergedAnnotations.search(TYPE_HIERARCHY); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketRequestValues.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketRequestValues.java index 4866b033f421..1666803f0889 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketRequestValues.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketRequestValues.java @@ -22,10 +22,10 @@ import java.util.List; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.StringUtils; @@ -41,21 +41,17 @@ */ public final class RSocketRequestValues { - @Nullable - private final String route; + private final @Nullable String route; private final Object[] routeVariables; private final Map metadata; - @Nullable - private final Object payloadValue; + private final @Nullable Object payloadValue; - @Nullable - private final Publisher payload; + private final @Nullable Publisher payload; - @Nullable - private final ParameterizedTypeReference payloadElementType; + private final @Nullable ParameterizedTypeReference payloadElementType; public RSocketRequestValues( @@ -76,8 +72,7 @@ public RSocketRequestValues( * Return the route value for * {@link org.springframework.messaging.rsocket.RSocketRequester#route(String, Object...) route}. */ - @Nullable - public String getRoute() { + public @Nullable String getRoute() { return this.route; } @@ -102,8 +97,7 @@ public Map getMetadata() { *

    This is mutually exclusive with {@link #getPayload()}. * Only one of the two or neither is set. */ - @Nullable - public Object getPayloadValue() { + public @Nullable Object getPayloadValue() { return this.payloadValue; } @@ -112,16 +106,14 @@ public Object getPayloadValue() { *

    This is mutually exclusive with {@link #getPayloadValue()}. * Only one of the two or neither is set. */ - @Nullable - public Publisher getPayload() { + public @Nullable Publisher getPayload() { return this.payload; } /** * Return the element type for a {@linkplain #getPayload() Publisher payload}. */ - @Nullable - public ParameterizedTypeReference getPayloadElementType() { + public @Nullable ParameterizedTypeReference getPayloadElementType() { return this.payloadElementType; } @@ -136,23 +128,17 @@ public static Builder builder(@Nullable String route) { */ public static final class Builder { - @Nullable - private String route; + private @Nullable String route; - @Nullable - private List routeVariables; + private @Nullable List routeVariables; - @Nullable - private MetadataHelper metadataHelper; + private @Nullable MetadataHelper metadataHelper; - @Nullable - private Object payloadValue; + private @Nullable Object payloadValue; - @Nullable - private Publisher payload; + private @Nullable Publisher payload; - @Nullable - private ParameterizedTypeReference payloadElementType; + private @Nullable ParameterizedTypeReference payloadElementType; Builder(@Nullable String route) { this.route = (StringUtils.hasText(route) ? route : null); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolver.java index a15b97e86b70..a26f671f1baf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolver.java @@ -16,8 +16,9 @@ package org.springframework.messaging.rsocket.service; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; /** * Resolve an argument from an {@link RSocketExchange @RSocketExchange}-annotated diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java index 1b6c87f81746..f15fa36790e2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceMethod.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,9 +21,11 @@ import java.util.Iterator; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Optional; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -34,7 +36,6 @@ import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.SynthesizingMethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.rsocket.RSocketRequester; import org.springframework.messaging.rsocket.RSocketStrategies; import org.springframework.util.Assert; @@ -59,8 +60,7 @@ final class RSocketServiceMethod { private final List argumentResolvers; - @Nullable - private final String route; + private final @Nullable String route; private final Function responseFunction; @@ -91,9 +91,8 @@ private static MethodParameter[] initMethodParameters(Method method) { return parameters; } - @Nullable - @SuppressWarnings("NullAway") - private static String initRoute( + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private static @Nullable String initRoute( Method method, Class containingClass, RSocketStrategies strategies, @Nullable StringValueResolver embeddedValueResolver) { @@ -168,7 +167,7 @@ else if (reactiveAdapter == null) { ((Mono) responsePublisher).blockOptional()); } else { - return (blockTimeout != null ? + return Objects.requireNonNull(blockTimeout != null ? ((Mono) responsePublisher).block(blockTimeout) : ((Mono) responsePublisher).block()); } @@ -217,14 +216,13 @@ public Method getMethod() { return this.method; } - @Nullable - public Object invoke(Object[] arguments) { + public @Nullable Object invoke(@Nullable Object[] arguments) { RSocketRequestValues.Builder requestValues = RSocketRequestValues.builder(this.route); applyArguments(requestValues, arguments); return this.responseFunction.apply(requestValues.build()); } - private void applyArguments(RSocketRequestValues.Builder requestValues, Object[] arguments) { + private void applyArguments(RSocketRequestValues.Builder requestValues, @Nullable Object[] arguments) { Assert.isTrue(arguments.length == this.parameters.length, "Method argument mismatch"); for (int i = 0; i < arguments.length; i++) { Object value = arguments[i]; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceProxyFactory.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceProxyFactory.java index bfab89ed5789..3d449795c624 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceProxyFactory.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/RSocketServiceProxyFactory.java @@ -27,13 +27,13 @@ import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; +import org.jspecify.annotations.Nullable; import org.springframework.aop.framework.ProxyFactory; import org.springframework.aop.framework.ReflectiveMethodInvocation; import org.springframework.core.MethodIntrospector; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.messaging.rsocket.RSocketRequester; import org.springframework.util.Assert; import org.springframework.util.StringValueResolver; @@ -54,13 +54,11 @@ public final class RSocketServiceProxyFactory { private final List argumentResolvers; - @Nullable - private final StringValueResolver embeddedValueResolver; + private final @Nullable StringValueResolver embeddedValueResolver; private final ReactiveAdapterRegistry reactiveAdapterRegistry; - @Nullable - private final Duration blockTimeout; + private final @Nullable Duration blockTimeout; private RSocketServiceProxyFactory( @@ -129,18 +127,15 @@ public static Builder builder() { */ public static final class Builder { - @Nullable - private RSocketRequester rsocketRequester; + private @Nullable RSocketRequester rsocketRequester; private final List customArgumentResolvers = new ArrayList<>(); - @Nullable - private StringValueResolver embeddedValueResolver; + private @Nullable StringValueResolver embeddedValueResolver; private ReactiveAdapterRegistry reactiveAdapterRegistry = ReactiveAdapterRegistry.getSharedInstance(); - @Nullable - private Duration blockTimeout; + private @Nullable Duration blockTimeout; private Builder() { } @@ -247,8 +242,7 @@ private ServiceMethodInterceptor(List methods) { } @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); RSocketServiceMethod serviceMethod = this.serviceMethods.get(method); if (serviceMethod != null) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/package-info.java index 65c6ba8c22df..e9dcd84d49c4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/rsocket/service/package-info.java @@ -3,9 +3,7 @@ * with a proxy factory backed by an * {@link org.springframework.messaging.rsocket.RSocketRequester}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.rsocket.service; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributes.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributes.java index 8946b6db20c6..7970fb17c48c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributes.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributes.java @@ -19,8 +19,8 @@ import java.util.Map; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; @@ -71,8 +71,7 @@ public SimpAttributes(String sessionId, Map attributes) { * @param name the name of the attribute * @return the current attribute value, or {@code null} if not found */ - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { return this.attributes.get(name); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributesContextHolder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributesContextHolder.java index 6474d56afce7..8acde1194265 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributesContextHolder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpAttributesContextHolder.java @@ -16,8 +16,9 @@ package org.springframework.messaging.simp; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedThreadLocal; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -66,8 +67,7 @@ public static void setAttributesFromMessage(Message message) { * Return the SimpAttributes currently bound to the thread. * @return the attributes or {@code null} if not bound */ - @Nullable - public static SimpAttributes getAttributes() { + public static @Nullable SimpAttributes getAttributes() { return attributesHolder.get(); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java index 015db5c8dc37..dde426e0d504 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageHeaderAccessor.java @@ -21,7 +21,8 @@ import java.util.Map; import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.support.IdTimestampMessageHeaderInitializer; import org.springframework.messaging.support.MessageHeaderAccessor; @@ -85,8 +86,7 @@ public class SimpMessageHeaderAccessor extends NativeMessageHeaderAccessor { public static final String IGNORE_ERROR = "simpIgnoreError"; - @Nullable - private Consumer userCallback; + private @Nullable Consumer userCallback; /** @@ -125,8 +125,7 @@ public void setMessageTypeIfNotSet(SimpMessageType messageType) { } } - @Nullable - public SimpMessageType getMessageType() { + public @Nullable SimpMessageType getMessageType() { return (SimpMessageType) getHeader(MESSAGE_TYPE_HEADER); } @@ -134,8 +133,7 @@ public void setDestination(@Nullable String destination) { setHeader(DESTINATION_HEADER, destination); } - @Nullable - public String getDestination() { + public @Nullable String getDestination() { return (String) getHeader(DESTINATION_HEADER); } @@ -143,8 +141,7 @@ public void setSubscriptionId(@Nullable String subscriptionId) { setHeader(SUBSCRIPTION_ID_HEADER, subscriptionId); } - @Nullable - public String getSubscriptionId() { + public @Nullable String getSubscriptionId() { return (String) getHeader(SUBSCRIPTION_ID_HEADER); } @@ -155,8 +152,7 @@ public void setSessionId(@Nullable String sessionId) { /** * Return the id of the current session. */ - @Nullable - public String getSessionId() { + public @Nullable String getSessionId() { return (String) getHeader(SESSION_ID_HEADER); } @@ -171,8 +167,7 @@ public void setSessionAttributes(@Nullable Map attributes) { * Return the attributes associated with the current session. */ @SuppressWarnings("unchecked") - @Nullable - public Map getSessionAttributes() { + public @Nullable Map getSessionAttributes() { return (Map) getHeader(SESSION_ATTRIBUTES); } @@ -186,8 +181,7 @@ public void setUser(@Nullable Principal principal) { /** * Return the user associated with the current session. */ - @Nullable - public Principal getUser() { + public @Nullable Principal getUser() { return (Principal) getHeader(USER_HEADER); } @@ -279,39 +273,32 @@ public static SimpMessageHeaderAccessor wrap(Message message) { return new SimpMessageHeaderAccessor(message); } - @Nullable - public static SimpMessageType getMessageType(Map headers) { + public static @Nullable SimpMessageType getMessageType(Map headers) { return (SimpMessageType) headers.get(MESSAGE_TYPE_HEADER); } - @Nullable - public static String getDestination(Map headers) { + public static @Nullable String getDestination(Map headers) { return (String) headers.get(DESTINATION_HEADER); } - @Nullable - public static String getSubscriptionId(Map headers) { + public static @Nullable String getSubscriptionId(Map headers) { return (String) headers.get(SUBSCRIPTION_ID_HEADER); } - @Nullable - public static String getSessionId(Map headers) { + public static @Nullable String getSessionId(Map headers) { return (String) headers.get(SESSION_ID_HEADER); } @SuppressWarnings("unchecked") - @Nullable - public static Map getSessionAttributes(Map headers) { + public static @Nullable Map getSessionAttributes(Map headers) { return (Map) headers.get(SESSION_ATTRIBUTES); } - @Nullable - public static Principal getUser(Map headers) { + public static @Nullable Principal getUser(Map headers) { return (Principal) headers.get(USER_HEADER); } - @Nullable - public static long[] getHeartbeat(Map headers) { + public static long @Nullable [] getHeartbeat(Map headers) { return (long[]) headers.get(HEART_BEAT_HEADER); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageMappingInfo.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageMappingInfo.java index 4523714ecccf..9131c291ada3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageMappingInfo.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageMappingInfo.java @@ -16,7 +16,8 @@ package org.springframework.messaging.simp; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.handler.CompositeMessageCondition; import org.springframework.messaging.handler.DestinationPatternsMessageCondition; @@ -64,8 +65,7 @@ public SimpMessageMappingInfo combine(SimpMessageMappingInfo other) { } @Override - @Nullable - public SimpMessageMappingInfo getMatchingCondition(Message message) { + public @Nullable SimpMessageMappingInfo getMatchingCondition(Message message) { CompositeMessageCondition condition = this.delegate.getMatchingCondition(message); return condition != null ? new SimpMessageMappingInfo(condition) : null; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageSendingOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageSendingOperations.java index e1cac48b59b8..11583e51390e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageSendingOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageSendingOperations.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessagingException; import org.springframework.messaging.core.MessagePostProcessor; import org.springframework.messaging.core.MessageSendingOperations; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageTypeMessageCondition.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageTypeMessageCondition.java index 522cd82fc700..e8bfa3145847 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageTypeMessageCondition.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessageTypeMessageCondition.java @@ -20,7 +20,8 @@ import java.util.Collections; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.handler.AbstractMessageCondition; import org.springframework.util.Assert; @@ -74,8 +75,7 @@ public SimpMessageTypeMessageCondition combine(SimpMessageTypeMessageCondition o } @Override - @Nullable - public SimpMessageTypeMessageCondition getMatchingCondition(Message message) { + public @Nullable SimpMessageTypeMessageCondition getMatchingCondition(Message message) { SimpMessageType actual = SimpMessageHeaderAccessor.getMessageType(message.getHeaders()); return (actual != null && actual.equals(this.messageType) ? this : null); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java index fdd5f357a771..89c7f64a6b1f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/SimpMessagingTemplate.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageDeliveryException; @@ -53,8 +54,7 @@ public class SimpMessagingTemplate extends AbstractMessageSendingTemplate objectFactory) { } @Override - @Nullable - public Object remove(String name) { + public @Nullable Object remove(String name) { SimpAttributes simpAttributes = SimpAttributesContextHolder.currentAttributes(); synchronized (simpAttributes.getSessionMutex()) { Object value = simpAttributes.getAttribute(name); @@ -71,8 +71,7 @@ public void registerDestructionCallback(String name, Runnable callback) { } @Override - @Nullable - public Object resolveContextualObject(String key) { + public @Nullable Object resolveContextualObject(String key) { return null; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java index c121def42dba..baf6f519294e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Annotations and for handling messages from Simple Messaging Protocols such as STOMP. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java index 524f4b973d2f..156f6e85193a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/PrincipalMethodArgumentResolver.java @@ -19,8 +19,9 @@ import java.security.Principal; import java.util.Optional; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.HandlerMethodArgumentResolver; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -41,8 +42,7 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - @Nullable - public Object resolveArgument(MethodParameter parameter, Message message){ + public @Nullable Object resolveArgument(MethodParameter parameter, Message message){ Principal user = SimpMessageHeaderAccessor.getUser(message.getHeaders()); return parameter.isOptional() ? Optional.ofNullable(user) : user; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java index 44bc35404c5f..042265f3a967 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SendToMethodReturnValueHandler.java @@ -21,10 +21,11 @@ import java.util.Collections; import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; @@ -70,8 +71,7 @@ public class SendToMethodReturnValueHandler implements HandlerMethodReturnValueH private final PropertyPlaceholderHelper placeholderHelper = new PropertyPlaceholderHelper("{", "}", null, null, false); - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; public SendToMethodReturnValueHandler(SimpMessageSendingOperations messagingTemplate, boolean annotationRequired) { @@ -129,8 +129,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia /** * Return the configured header initializer. */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } @@ -208,8 +207,7 @@ private DestinationHelper getDestinationHelper(MessageHeaders headers, MethodPar new DestinationHelper(headers, m1, m2) : new DestinationHelper(headers, c1, c2)); } - @Nullable - protected String getUserName(Message message, MessageHeaders headers) { + protected @Nullable String getUserName(Message message, MessageHeaders headers) { Principal principal = SimpMessageHeaderAccessor.getUser(headers); if (principal != null) { return (principal instanceof DestinationUserNameProvider provider ? @@ -260,11 +258,9 @@ private class DestinationHelper { private final PlaceholderResolver placeholderResolver; - @Nullable - private final SendTo sendTo; + private final @Nullable SendTo sendTo; - @Nullable - private final SendToUser sendToUser; + private final @Nullable SendToUser sendToUser; public DestinationHelper(MessageHeaders headers, @Nullable SendToUser sendToUser, @Nullable SendTo sendTo) { @@ -280,13 +276,11 @@ private Map getTemplateVariables(MessageHeaders headers) { return (Map) headers.getOrDefault(name, Collections.emptyMap()); } - @Nullable - public SendTo getSendTo() { + public @Nullable SendTo getSendTo() { return this.sendTo; } - @Nullable - public SendToUser getSendToUser() { + public @Nullable SendToUser getSendToUser() { return this.sendToUser; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java index 41ecfb1730e3..ce19fca640b3 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandler.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,9 +23,11 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.Set; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ApplicationContext; @@ -35,7 +37,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.convert.ConversionService; import org.springframework.format.support.DefaultFormattingConversionService; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; @@ -111,17 +112,13 @@ public class SimpAnnotationMethodMessageHandler extends AbstractMethodMessageHan private boolean slashPathSeparator = true; - @Nullable - private Validator validator; + private @Nullable Validator validator; - @Nullable - private StringValueResolver valueResolver; + private @Nullable StringValueResolver valueResolver; - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; - @Nullable - private Integer phase; + private @Nullable Integer phase; private volatile boolean running; @@ -167,8 +164,7 @@ public void setDestinationPrefixes(@Nullable Collection prefixes) { super.setDestinationPrefixes(appendSlashes(prefixes)); } - @Nullable - private static Collection appendSlashes(@Nullable Collection prefixes) { + private static @Nullable Collection appendSlashes(@Nullable Collection prefixes) { if (CollectionUtils.isEmpty(prefixes)) { return prefixes; } @@ -237,8 +233,7 @@ public PathMatcher getPathMatcher() { /** * Return the configured Validator instance. */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return this.validator; } @@ -269,8 +264,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia /** * Return the configured header initializer. */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } @@ -344,13 +338,11 @@ protected List initArgumentResolvers() { } @Override - @SuppressWarnings("removal") protected List initReturnValueHandlers() { List handlers = new ArrayList<>(); // Single-purpose return value types - handlers.add(new org.springframework.messaging.handler.invocation.ListenableFutureReturnValueHandler()); handlers.add(new CompletableFutureReturnValueHandler()); if (reactorPresent) { handlers.add(new ReactiveReturnValueHandler()); @@ -398,8 +390,7 @@ protected boolean isHandler(Class beanType) { } @Override - @Nullable - protected SimpMessageMappingInfo getMappingForMethod(Method method, Class handlerType) { + protected @Nullable SimpMessageMappingInfo getMappingForMethod(Method method, Class handlerType) { MessageMapping messageAnn = AnnotatedElementUtils.findMergedAnnotation(method, MessageMapping.class); if (messageAnn != null) { MessageMapping typeAnn = AnnotatedElementUtils.findMergedAnnotation(handlerType, MessageMapping.class); @@ -454,7 +445,7 @@ protected String[] resolveEmbeddedValuesInDestinations(String[] destinations) { } String[] result = new String[destinations.length]; for (int i = 0; i < destinations.length; i++) { - result[i] = this.valueResolver.resolveStringValue(destinations[i]); + result[i] = Objects.requireNonNull(this.valueResolver.resolveStringValue(destinations[i])); } return result; } @@ -471,14 +462,12 @@ protected Set getDirectLookupDestinations(SimpMessageMappingInfo mapping } @Override - @Nullable - protected String getDestination(Message message) { + protected @Nullable String getDestination(Message message) { return SimpMessageHeaderAccessor.getDestination(message.getHeaders()); } @Override - @Nullable - protected String getLookupDestination(@Nullable String destination) { + protected @Nullable String getLookupDestination(@Nullable String destination) { if (destination == null) { return null; } @@ -499,8 +488,7 @@ protected String getLookupDestination(@Nullable String destination) { } @Override - @Nullable - protected SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message message) { + protected @Nullable SimpMessageMappingInfo getMatchingMapping(SimpMessageMappingInfo mapping, Message message) { return mapping.getMatchingCondition(message); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java index 8ff4d521a2fe..f6d316ec8145 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/SubscriptionMethodReturnValueHandler.java @@ -17,9 +17,9 @@ package org.springframework.messaging.simp.annotation.support; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.core.AbstractMessageSendingTemplate; @@ -63,8 +63,7 @@ public class SubscriptionMethodReturnValueHandler implements HandlerMethodReturn private final MessageSendingOperations messagingTemplate; - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; /** @@ -90,8 +89,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia /** * Return the configured header initializer. */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/package-info.java index bc4b50f166d1..c5810b2e5507 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/annotation/support/package-info.java @@ -2,9 +2,7 @@ * Support classes for handling messages from simple messaging protocols * (like STOMP). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp.annotation.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java index 7c17734e0652..7ddf02c8252e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/AbstractBrokerMessageHandler.java @@ -22,11 +22,11 @@ import java.util.function.Predicate; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.context.ApplicationEventPublisherAware; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -59,13 +59,11 @@ public abstract class AbstractBrokerMessageHandler private final Collection destinationPrefixes; - @Nullable - private Predicate userDestinationPredicate; + private @Nullable Predicate userDestinationPredicate; private boolean preservePublishOrder = false; - @Nullable - private ApplicationEventPublisher eventPublisher; + private @Nullable ApplicationEventPublisher eventPublisher; private final AtomicBoolean brokerAvailable = new AtomicBoolean(); @@ -75,8 +73,7 @@ public abstract class AbstractBrokerMessageHandler private boolean autoStartup = true; - @Nullable - private Integer phase; + private @Nullable Integer phase; private volatile boolean running; @@ -186,8 +183,7 @@ public void setApplicationEventPublisher(@Nullable ApplicationEventPublisher pub this.eventPublisher = publisher; } - @Nullable - public ApplicationEventPublisher getApplicationEventPublisher() { + public @Nullable ApplicationEventPublisher getApplicationEventPublisher() { return this.eventPublisher; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/BrokerAvailabilityEvent.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/BrokerAvailabilityEvent.java index 31e8b1ea8cdf..a3e5e977364e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/BrokerAvailabilityEvent.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/BrokerAvailabilityEvent.java @@ -33,8 +33,8 @@ public class BrokerAvailabilityEvent extends ApplicationEvent { /** * Creates a new {@code BrokerAvailabilityEvent}. * - * @param brokerAvailable {@code true} if the broker is available, {@code} - * false otherwise + * @param brokerAvailable {@code true} if the broker is available, {@code + * false} otherwise * @param source the component that is acting as the broker, or as a relay * for an external broker, that has changed availability. Must not be {@code * null}. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java index 078ed325a7ac..76798f769a09 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java @@ -28,6 +28,8 @@ import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; + import org.springframework.expression.EvaluationContext; import org.springframework.expression.Expression; import org.springframework.expression.ExpressionParser; @@ -36,7 +38,6 @@ import org.springframework.expression.spel.SpelEvaluationException; import org.springframework.expression.spel.standard.SpelExpressionParser; import org.springframework.expression.spel.support.SimpleEvaluationContext; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -80,8 +81,7 @@ public class DefaultSubscriptionRegistry extends AbstractSubscriptionRegistry { private int cacheLimit = DEFAULT_CACHE_LIMIT; - @Nullable - private String selectorHeaderName; + private @Nullable String selectorHeaderName; private volatile boolean selectorHeaderInUse; @@ -148,8 +148,7 @@ public void setSelectorHeaderName(@Nullable String selectorHeaderName) { * @since 4.2 * @see #setSelectorHeaderName(String) */ - @Nullable - public String getSelectorHeaderName() { + public @Nullable String getSelectorHeaderName() { return this.selectorHeaderName; } @@ -165,8 +164,7 @@ protected void addSubscriptionInternal( this.destinationCache.updateAfterNewSubscription(sessionId, subscription); } - @Nullable - private Expression getSelectorExpression(MessageHeaders headers) { + private @Nullable Expression getSelectorExpression(MessageHeaders headers) { if (getSelectorHeaderName() == null) { return null; } @@ -395,8 +393,7 @@ private static final class SessionRegistry { private final ConcurrentMap sessions = new ConcurrentHashMap<>(); - @Nullable - public SessionInfo getSession(String sessionId) { + public @Nullable SessionInfo getSession(String sessionId) { return this.sessions.get(sessionId); } @@ -410,8 +407,7 @@ public void addSubscription(String sessionId, Subscription subscription) { info.addSubscription(subscription); } - @Nullable - public SessionInfo removeSubscriptions(String sessionId) { + public @Nullable SessionInfo removeSubscriptions(String sessionId) { return this.sessions.remove(sessionId); } } @@ -428,8 +424,7 @@ public Collection getSubscriptions() { return this.subscriptionMap.values(); } - @Nullable - public Subscription getSubscription(String subscriptionId) { + public @Nullable Subscription getSubscription(String subscriptionId) { return this.subscriptionMap.get(subscriptionId); } @@ -437,8 +432,7 @@ public void addSubscription(Subscription subscription) { this.subscriptionMap.putIfAbsent(subscription.getId(), subscription); } - @Nullable - public Subscription removeSubscription(String subscriptionId) { + public @Nullable Subscription removeSubscription(String subscriptionId) { return this.subscriptionMap.remove(subscriptionId); } } @@ -454,8 +448,7 @@ private static final class Subscription { private final boolean isPattern; - @Nullable - private final Expression selector; + private final @Nullable Expression selector; public Subscription(String id, String destination, boolean isPattern, @Nullable Expression selector) { Assert.notNull(id, "Subscription id must not be null"); @@ -478,8 +471,7 @@ public boolean isPattern() { return this.isPattern; } - @Nullable - public Expression getSelector() { + public @Nullable Expression getSelector() { return this.selector; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/OrderedMessageChannelDecorator.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/OrderedMessageChannelDecorator.java index 5d26ccbea72f..0d20c2c71c20 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/OrderedMessageChannelDecorator.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/OrderedMessageChannelDecorator.java @@ -22,8 +22,8 @@ import java.util.concurrent.atomic.AtomicInteger; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -172,8 +172,7 @@ public static boolean supportsOrderedMessages(MessageChannel channel) { /** * Obtain the task to release the next message, if found. */ - @Nullable - public static Runnable getNextMessageTask(Message message) { + public static @Nullable Runnable getNextMessageTask(Message message) { return (Runnable) message.getHeaders().get(OrderedMessageChannelDecorator.NEXT_MESSAGE_TASK_HEADER); } @@ -185,8 +184,7 @@ private final class PostHandleTask implements Runnable { private final Message message; - @Nullable - private final AtomicInteger handledCount; + private final @Nullable AtomicInteger handledCount; private PostHandleTask(Message message) { this.message = message; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java index 80bcfe99e473..bd1703c89e81 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/SimpleBrokerMessageHandler.java @@ -24,7 +24,8 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ScheduledFuture; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; @@ -54,31 +55,24 @@ public class SimpleBrokerMessageHandler extends AbstractBrokerMessageHandler { private static final byte[] EMPTY_PAYLOAD = new byte[0]; - @Nullable - private PathMatcher pathMatcher; + private @Nullable PathMatcher pathMatcher; - @Nullable - private Integer cacheLimit; + private @Nullable Integer cacheLimit; - @Nullable - private String selectorHeaderName; + private @Nullable String selectorHeaderName; - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; - @Nullable - private long[] heartbeatValue; + private long @Nullable [] heartbeatValue; - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; private SubscriptionRegistry subscriptionRegistry; private final Map sessions = new ConcurrentHashMap<>(); - @Nullable - private ScheduledFuture heartbeatFuture; + private @Nullable ScheduledFuture heartbeatFuture; /** @@ -212,8 +206,7 @@ public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { * Return the configured TaskScheduler. * @since 4.2 */ - @Nullable - public TaskScheduler getTaskScheduler() { + public @Nullable TaskScheduler getTaskScheduler() { return this.taskScheduler; } @@ -226,7 +219,7 @@ public TaskScheduler getTaskScheduler() { * (in milliseconds). * @since 4.2 */ - public void setHeartbeatValue(@Nullable long[] heartbeat) { + public void setHeartbeatValue(long @Nullable [] heartbeat) { if (heartbeat != null && (heartbeat.length != 2 || heartbeat[0] < 0 || heartbeat[1] < 0)) { throw new IllegalArgumentException("Invalid heart-beat: " + Arrays.toString(heartbeat)); } @@ -237,8 +230,7 @@ public void setHeartbeatValue(@Nullable long[] heartbeat) { * The configured value for the heart-beat settings. * @since 4.2 */ - @Nullable - public long[] getHeartbeatValue() { + public long @Nullable [] getHeartbeatValue() { return this.heartbeatValue; } @@ -256,8 +248,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia * Return the configured header initializer. * @since 4.1 */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } @@ -444,8 +435,7 @@ private static class SessionInfo { private final String sessionId; - @Nullable - private final Principal user; + private final @Nullable Principal user; private final MessageChannel clientOutboundChannel; @@ -459,7 +449,7 @@ private static class SessionInfo { public SessionInfo(String sessionId, @Nullable Principal user, MessageChannel outboundChannel, - @Nullable long[] clientHeartbeat, @Nullable long[] serverHeartbeat) { + long @Nullable [] clientHeartbeat, long @Nullable [] serverHeartbeat) { this.sessionId = sessionId; this.user = user; @@ -481,8 +471,7 @@ public String getSessionId() { return this.sessionId; } - @Nullable - public Principal getUser() { + public @Nullable Principal getUser() { return this.user; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/package-info.java index afc2f7016ca5..3059a03c42ec 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/broker/package-info.java @@ -2,9 +2,7 @@ * Provides a "simple" message broker implementation along with an abstract base * class and other supporting types such as a registry for subscriptions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp.broker; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java index 37c2d3b40022..194bc027f9e4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/AbstractBrokerRegistration.java @@ -21,7 +21,8 @@ import java.util.Collections; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.simp.broker.AbstractBrokerMessageHandler; @@ -49,7 +50,7 @@ public abstract class AbstractBrokerRegistration { * @param destinationPrefixes the destination prefixes */ public AbstractBrokerRegistration(SubscribableChannel clientInboundChannel, - MessageChannel clientOutboundChannel, @Nullable String[] destinationPrefixes) { + MessageChannel clientOutboundChannel, String @Nullable [] destinationPrefixes) { Assert.notNull(clientInboundChannel, "'clientInboundChannel' must not be null"); Assert.notNull(clientOutboundChannel, "'clientOutboundChannel' must not be null"); 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 9c505329dc65..7ccf4664efad 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 @@ -24,18 +24,20 @@ import java.util.concurrent.Executor; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.BeanInitializationException; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.context.annotation.Bean; import org.springframework.context.event.SmartApplicationListener; -import org.springframework.lang.Nullable; import org.springframework.messaging.MessageHandler; 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; @@ -102,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; @@ -113,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); @@ -121,20 +126,15 @@ public abstract class AbstractMessageBrokerConfiguration implements ApplicationC } - @Nullable - private ApplicationContext applicationContext; + private @Nullable ApplicationContext applicationContext; - @Nullable - private ChannelRegistration clientInboundChannelRegistration; + private @Nullable ChannelRegistration clientInboundChannelRegistration; - @Nullable - private ChannelRegistration clientOutboundChannelRegistration; + private @Nullable ChannelRegistration clientOutboundChannelRegistration; - @Nullable - private MessageBrokerRegistry brokerRegistry; + private @Nullable MessageBrokerRegistry brokerRegistry; - @Nullable - private Integer phase; + private @Nullable Integer phase; /** @@ -149,8 +149,7 @@ public void setApplicationContext(@Nullable ApplicationContext applicationContex this.applicationContext = applicationContext; } - @Nullable - public ApplicationContext getApplicationContext() { + public @Nullable ApplicationContext getApplicationContext() { return this.applicationContext; } @@ -324,8 +323,7 @@ protected void configureMessageBroker(MessageBrokerRegistry registry) { * Provide access to the configured PatchMatcher for access from other * configuration classes. */ - @Nullable - public final PathMatcher getPathMatcher( + public final @Nullable PathMatcher getPathMatcher( AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel) { return getBrokerRegistry(clientInboundChannel, clientOutboundChannel).getPathMatcher(); @@ -385,8 +383,7 @@ protected void addReturnValueHandlers(List retu } @Bean - @Nullable - public AbstractBrokerMessageHandler simpleBrokerMessageHandler( + public @Nullable AbstractBrokerMessageHandler simpleBrokerMessageHandler( AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, AbstractSubscribableChannel brokerChannel, UserDestinationResolver userDestinationResolver) { @@ -414,8 +411,7 @@ private void updateUserDestinationResolver( } @Bean - @Nullable - public AbstractBrokerMessageHandler stompBrokerRelayMessageHandler( + public @Nullable AbstractBrokerMessageHandler stompBrokerRelayMessageHandler( AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, AbstractSubscribableChannel brokerChannel, UserDestinationMessageHandler userDestinationMessageHandler, @Nullable MessageHandler userRegistryMessageHandler, UserDestinationResolver userDestinationResolver) { @@ -458,8 +454,7 @@ public UserDestinationMessageHandler userDestinationMessageHandler( } @Bean - @Nullable - public MessageHandler userRegistryMessageHandler( + public @Nullable MessageHandler userRegistryMessageHandler( AbstractSubscribableChannel clientInboundChannel, AbstractSubscribableChannel clientOutboundChannel, SimpUserRegistry userRegistry, SimpMessagingTemplate brokerMessagingTemplate, @Qualifier("messageBrokerTaskScheduler") TaskScheduler scheduler) { @@ -510,7 +505,10 @@ public CompositeMessageConverter brokerMessageConverter() { if (kotlinSerializationJsonPresent) { converters.add(new KotlinSerializationJsonMessageConverter()); } - if (jackson2Present) { + if (jacksonPresent) { + converters.add(createJacksonJsonConverter()); + } + else if (jackson2Present) { converters.add(createJacksonConverter()); } else if (gsonPresent) { @@ -523,6 +521,21 @@ else if (jsonbPresent) { 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); @@ -618,8 +631,7 @@ public void validate(@Nullable Object target, Errors errors) { * Override this method to provide a custom {@link Validator}. * @since 4.0.1 */ - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return null; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java index ad8649587ace..f6d63f0e9e3d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/ChannelRegistration.java @@ -23,7 +23,8 @@ import java.util.function.Consumer; import java.util.function.Supplier; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.support.ChannelInterceptor; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; @@ -37,11 +38,9 @@ */ public class ChannelRegistration { - @Nullable - private TaskExecutorRegistration registration; + private @Nullable TaskExecutorRegistration registration; - @Nullable - private Executor executor; + private @Nullable Executor executor; private final List interceptors = new ArrayList<>(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java index 45caca7de75f..a2892b4a56d6 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/MessageBrokerRegistry.java @@ -19,8 +19,9 @@ import java.util.Arrays; import java.util.Collection; +import org.jspecify.annotations.Nullable; + import org.springframework.context.event.SmartApplicationListener; -import org.springframework.lang.Nullable; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; @@ -41,28 +42,21 @@ public class MessageBrokerRegistry { private final MessageChannel clientOutboundChannel; - @Nullable - private SimpleBrokerRegistration simpleBrokerRegistration; + private @Nullable SimpleBrokerRegistration simpleBrokerRegistration; - @Nullable - private StompBrokerRelayRegistration brokerRelayRegistration; + private @Nullable StompBrokerRelayRegistration brokerRelayRegistration; private final ChannelRegistration brokerChannelRegistration = new ChannelRegistration(); - @Nullable - private String[] applicationDestinationPrefixes; + private String @Nullable [] applicationDestinationPrefixes; - @Nullable - private String userDestinationPrefix; + private @Nullable String userDestinationPrefix; - @Nullable - private Integer userRegistryOrder; + private @Nullable Integer userRegistryOrder; - @Nullable - private PathMatcher pathMatcher; + private @Nullable PathMatcher pathMatcher; - @Nullable - private Integer cacheLimit; + private @Nullable Integer cacheLimit; private boolean preservePublishOrder; @@ -111,14 +105,12 @@ protected ChannelRegistration getBrokerChannelRegistration() { return this.brokerChannelRegistration; } - @Nullable - protected String getUserDestinationBroadcast() { + protected @Nullable String getUserDestinationBroadcast() { return (this.brokerRelayRegistration != null ? this.brokerRelayRegistration.getUserDestinationBroadcast() : null); } - @Nullable - protected String getUserRegistryBroadcast() { + protected @Nullable String getUserRegistryBroadcast() { return (this.brokerRelayRegistration != null ? this.brokerRelayRegistration.getUserRegistryBroadcast() : null); } @@ -138,8 +130,7 @@ public MessageBrokerRegistry setApplicationDestinationPrefixes(String... prefixe return this; } - @Nullable - protected Collection getApplicationDestinationPrefixes() { + protected @Nullable Collection getApplicationDestinationPrefixes() { return (this.applicationDestinationPrefixes != null ? Arrays.asList(this.applicationDestinationPrefixes) : null); } @@ -161,8 +152,7 @@ public MessageBrokerRegistry setUserDestinationPrefix(String destinationPrefix) return this; } - @Nullable - protected String getUserDestinationPrefix() { + protected @Nullable String getUserDestinationPrefix() { return this.userDestinationPrefix; } @@ -177,8 +167,7 @@ public void setUserRegistryOrder(int order) { this.userRegistryOrder = order; } - @Nullable - protected Integer getUserRegistryOrder() { + protected @Nullable Integer getUserRegistryOrder() { return this.userRegistryOrder; } @@ -204,8 +193,7 @@ public MessageBrokerRegistry setPathMatcher(PathMatcher pathMatcher) { return this; } - @Nullable - protected PathMatcher getPathMatcher() { + protected @Nullable PathMatcher getPathMatcher() { return this.pathMatcher; } @@ -236,8 +224,7 @@ public MessageBrokerRegistry setPreservePublishOrder(boolean preservePublishOrde return this; } - @Nullable - protected SimpleBrokerMessageHandler getSimpleBroker(SubscribableChannel brokerChannel) { + protected @Nullable SimpleBrokerMessageHandler getSimpleBroker(SubscribableChannel brokerChannel) { if (this.simpleBrokerRegistration == null && this.brokerRelayRegistration == null) { enableSimpleBroker(); } @@ -251,8 +238,7 @@ protected SimpleBrokerMessageHandler getSimpleBroker(SubscribableChannel brokerC return null; } - @Nullable - protected StompBrokerRelayMessageHandler getStompBrokerRelay(SubscribableChannel brokerChannel) { + protected @Nullable StompBrokerRelayMessageHandler getStompBrokerRelay(SubscribableChannel brokerChannel) { if (this.brokerRelayRegistration != null) { StompBrokerRelayMessageHandler relay = this.brokerRelayRegistration.getMessageHandler(brokerChannel); relay.setPreservePublishOrder(this.preservePublishOrder); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java index 628914a9d78c..99b14b5ad130 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/SimpleBrokerRegistration.java @@ -16,7 +16,8 @@ package org.springframework.messaging.simp.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.simp.broker.SimpleBrokerMessageHandler; @@ -31,14 +32,11 @@ */ public class SimpleBrokerRegistration extends AbstractBrokerRegistration { - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; - @Nullable - private long[] heartbeat; + private long @Nullable [] heartbeat; - @Nullable - private String selectorHeaderName; + private @Nullable String selectorHeaderName; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java index 417a4dcab50c..333651492f41 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/StompBrokerRelayRegistration.java @@ -16,7 +16,8 @@ package org.springframework.messaging.simp.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessageChannel; import org.springframework.messaging.SubscribableChannel; import org.springframework.messaging.simp.stomp.StompBrokerRelayMessageHandler; @@ -44,28 +45,21 @@ public class StompBrokerRelayRegistration extends AbstractBrokerRegistration { private String systemPasscode = "guest"; - @Nullable - private Long systemHeartbeatSendInterval; + private @Nullable Long systemHeartbeatSendInterval; - @Nullable - private Long systemHeartbeatReceiveInterval; + private @Nullable Long systemHeartbeatReceiveInterval; - @Nullable - private String virtualHost; + private @Nullable String virtualHost; - @Nullable - private TcpOperations tcpClient; + private @Nullable TcpOperations tcpClient; - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; private boolean autoStartup = true; - @Nullable - private String userDestinationBroadcast; + private @Nullable String userDestinationBroadcast; - @Nullable - private String userRegistryBroadcast; + private @Nullable String userRegistryBroadcast; /** @@ -235,8 +229,7 @@ public StompBrokerRelayRegistration setUserDestinationBroadcast(String destinati return this; } - @Nullable - protected String getUserDestinationBroadcast() { + protected @Nullable String getUserDestinationBroadcast() { return this.userDestinationBroadcast; } @@ -254,8 +247,7 @@ public StompBrokerRelayRegistration setUserRegistryBroadcast(String destination) return this; } - @Nullable - protected String getUserRegistryBroadcast() { + protected @Nullable String getUserRegistryBroadcast() { return this.userRegistryBroadcast; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java index f9cfdb07ad2a..89141906fe12 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/TaskExecutorRegistration.java @@ -16,7 +16,8 @@ package org.springframework.messaging.simp.config; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import org.springframework.util.Assert; @@ -33,17 +34,13 @@ public class TaskExecutorRegistration { private final ThreadPoolTaskExecutor taskExecutor; - @Nullable - private Integer corePoolSize; + private @Nullable Integer corePoolSize; - @Nullable - private Integer maxPoolSize; + private @Nullable Integer maxPoolSize; - @Nullable - private Integer keepAliveSeconds; + private @Nullable Integer keepAliveSeconds; - @Nullable - private Integer queueCapacity; + private @Nullable Integer queueCapacity; /** diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/package-info.java index 09947fad0c32..21b960eb753a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/config/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/config/package-info.java @@ -1,9 +1,7 @@ /** * Configuration support for WebSocket messaging using higher level messaging protocols. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/package-info.java index 2907d6c3322b..c6b727907e8f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/package-info.java @@ -1,9 +1,7 @@ /** * Generic support for Simple Messaging Protocols including protocols such as STOMP. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java index b7c9e1e32238..ad57a7a265d5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/BufferingStompDecoder.java @@ -22,7 +22,8 @@ import java.util.Queue; import java.util.concurrent.LinkedBlockingQueue; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; @@ -52,8 +53,7 @@ public class BufferingStompDecoder { private final Queue chunks = new LinkedBlockingQueue<>(); - @Nullable - private volatile Integer expectedContentLength; + private volatile @Nullable Integer expectedContentLength; /** @@ -163,8 +163,7 @@ public int getBufferSize() { /** * Get the expected content length of the currently buffered, incomplete STOMP frame. */ - @Nullable - public Integer getExpectedContentLength() { + public @Nullable Integer getExpectedContentLength() { return this.expectedContentLength; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java index 40faa0bcd59d..77befefeb96f 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ConnectionHandlingStompSession.java @@ -31,17 +31,6 @@ */ public interface ConnectionHandlingStompSession extends StompSession, StompTcpConnectionHandler { - /** - * Return a future that will complete when the session is ready for use. - * @deprecated as of 6.0, in favor of {@link #getSession()} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - default org.springframework.util.concurrent.ListenableFuture getSessionFuture() { - return new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>( - getSession()); - } - /** * Return a future that will complete when the session is ready for use. * @since 6.0 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 3ecbe0a799e9..84b549aaa407 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 @@ -30,9 +30,9 @@ import java.util.function.Consumer; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.converter.MessageConversionException; @@ -87,19 +87,16 @@ public class DefaultStompSession implements ConnectionHandlingStompSession { private MessageConverter converter = new SimpleMessageConverter(); - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; private long receiptTimeLimit = TimeUnit.SECONDS.toMillis(15); private volatile boolean autoReceiptEnabled; - @Nullable - private volatile TcpConnection connection; + private volatile @Nullable TcpConnection connection; - @Nullable - private volatile String version; + private volatile @Nullable String version; private final AtomicInteger subscriptionIndex = new AtomicInteger(); @@ -179,8 +176,7 @@ public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { /** * Return the configured TaskScheduler to use for receipt tracking. */ - @Nullable - public TaskScheduler getTaskScheduler() { + public @Nullable TaskScheduler getTaskScheduler() { return this.taskScheduler; } @@ -240,8 +236,7 @@ public Receiptable send(StompHeaders headers, Object payload) { return receiptable; } - @Nullable - private String checkOrAddReceipt(StompHeaders headers) { + private @Nullable String checkOrAddReceipt(StompHeaders headers) { String receiptId = headers.getReceipt(); if (isAutoReceiptEnabled() && receiptId == null) { receiptId = String.valueOf(DefaultStompSession.this.receiptIndex.getAndIncrement()); @@ -540,21 +535,17 @@ private void resetConnection() { private class ReceiptHandler implements Receiptable { - @Nullable - private final String receiptId; + private final @Nullable String receiptId; private final List> receiptCallbacks = new ArrayList<>(2); private final List receiptLostCallbacks = new ArrayList<>(2); - @Nullable - private ScheduledFuture future; + private @Nullable ScheduledFuture future; - @Nullable - private Boolean result; + private @Nullable Boolean result; - @Nullable - private StompHeaders receiptHeaders; + private @Nullable StompHeaders receiptHeaders; public ReceiptHandler(@Nullable String receiptId) { this.receiptId = receiptId; @@ -571,8 +562,7 @@ private void initReceiptHandling() { } @Override - @Nullable - public String getReceiptId() { + public @Nullable String getReceiptId() { return this.receiptId; } @@ -671,8 +661,7 @@ public DefaultSubscription(StompHeaders headers, StompFrameHandler handler) { } @Override - @Nullable - public String getSubscriptionId() { + public @Nullable String getSubscriptionId() { return this.headers.getId(); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java index d16ab82b3282..44a203393d0e 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClient.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,34 +18,21 @@ import java.util.concurrent.CompletableFuture; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.simp.SimpLogging; import org.springframework.messaging.tcp.TcpOperations; -import org.springframework.messaging.tcp.reactor.ReactorNetty2TcpClient; import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** - * A STOMP over TCP client, configurable with either - * {@link ReactorNettyTcpClient} or {@link ReactorNetty2TcpClient}. + * A STOMP over TCP client, configurable with {@link ReactorNettyTcpClient}. * * @author Rossen Stoyanchev * @since 5.0 */ public class ReactorNettyTcpStompClient extends StompClientSupport { - private static final boolean reactorNettyClientPresent; - - private static final boolean reactorNetty2ClientPresent; - - static { - ClassLoader classLoader = StompBrokerRelayMessageHandler.class.getClassLoader(); - reactorNettyClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", classLoader); - reactorNetty2ClientPresent = ClassUtils.isPresent("reactor.netty5.http.client.HttpClient", classLoader); - } - - private final TcpOperations tcpClient; @@ -75,36 +62,12 @@ public ReactorNettyTcpStompClient(TcpOperations tcpClient) { } private static TcpOperations initTcpClient(String host, int port) { - if (reactorNettyClientPresent) { - ReactorNettyTcpClient client = new ReactorNettyTcpClient<>(host, port, new StompReactorNettyCodec()); - client.setLogger(SimpLogging.forLog(client.getLogger())); - return client; - } - else if (reactorNetty2ClientPresent) { - ReactorNetty2TcpClient client = new ReactorNetty2TcpClient<>(host, port, new StompTcpMessageCodec()); - client.setLogger(SimpLogging.forLog(client.getLogger())); - return client; - } - throw new IllegalStateException("No compatible version of Reactor Netty"); + ReactorNettyTcpClient client = new ReactorNettyTcpClient<>(host, port, new StompReactorNettyCodec()); + client.setLogger(SimpLogging.forLog(client.getLogger())); + return client; } - /** - * Connect and notify the given {@link StompSessionHandler} when connected - * on the STOMP level. - * @param handler the handler for the STOMP session - * @return a ListenableFuture for access to the session when ready for use - * @deprecated as of 6.0, in favor of {@link #connectAsync(StompSessionHandler)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - public org.springframework.util.concurrent.ListenableFuture connect( - StompSessionHandler handler) { - - return new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>( - connectAsync(handler)); - } - /** * Connect and notify the given {@link StompSessionHandler} when connected * on the STOMP level. @@ -116,24 +79,6 @@ public CompletableFuture connectAsync(StompSessionHandler handler) return connectAsync(null, handler); } - /** - * An overloaded version of {@link #connect(StompSessionHandler)} that - * accepts headers to use for the STOMP CONNECT frame. - * @param connectHeaders headers to add to the CONNECT frame - * @param handler the handler for the STOMP session - * @return a ListenableFuture for access to the session when ready for use - * @deprecated as of 6.0, in favor of {@link #connectAsync(StompHeaders, StompSessionHandler)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - public org.springframework.util.concurrent.ListenableFuture connect( - @Nullable StompHeaders connectHeaders, StompSessionHandler handler) { - - ConnectionHandlingStompSession session = createSession(connectHeaders, handler); - this.tcpClient.connectAsync(session); - return session.getSessionFuture(); - } - /** * An overloaded version of {@link #connectAsync(StompSessionHandler)} that * accepts headers to use for the STOMP CONNECT frame. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java index 811157d51a28..e2697d464b87 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompBrokerRelayMessageHandler.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,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageDeliveryException; @@ -43,13 +44,10 @@ import org.springframework.messaging.tcp.FixedIntervalReconnectStrategy; import org.springframework.messaging.tcp.TcpConnection; import org.springframework.messaging.tcp.TcpOperations; -import org.springframework.messaging.tcp.reactor.ReactorNetty2TcpClient; import org.springframework.messaging.tcp.reactor.ReactorNettyCodec; import org.springframework.messaging.tcp.reactor.ReactorNettyTcpClient; -import org.springframework.messaging.tcp.reactor.TcpMessageCodec; import org.springframework.scheduling.TaskScheduler; import org.springframework.util.Assert; -import org.springframework.util.ClassUtils; /** * A {@link org.springframework.messaging.MessageHandler} that handles messages by @@ -104,18 +102,10 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler private static final Message HEARTBEAT_MESSAGE; - private static final boolean reactorNettyClientPresent; - - private static final boolean reactorNetty2ClientPresent; - static { HEART_BEAT_ACCESSOR = StompHeaderAccessor.createForHeartbeat(); HEARTBEAT_MESSAGE = MessageBuilder.createMessage( StompDecoder.HEARTBEAT_PAYLOAD, HEART_BEAT_ACCESSOR.getMessageHeaders()); - - ClassLoader classLoader = StompBrokerRelayMessageHandler.class.getClassLoader(); - reactorNettyClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", classLoader); - reactorNetty2ClientPresent = ClassUtils.isPresent("reactor.netty5.http.client.HttpClient", classLoader); } @@ -137,21 +127,17 @@ public class StompBrokerRelayMessageHandler extends AbstractBrokerMessageHandler private final Map systemSubscriptions = new HashMap<>(4); - @Nullable - private String virtualHost; + private @Nullable String virtualHost; - @Nullable - private TcpOperations tcpClient; + private @Nullable TcpOperations tcpClient; - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; private final DefaultStats stats = new DefaultStats(); private final Map connectionHandlers = new ConcurrentHashMap<>(); - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; /** @@ -349,15 +335,13 @@ public void setVirtualHost(@Nullable String virtualHost) { /** * Return the configured virtual host value. */ - @Nullable - public String getVirtualHost() { + public @Nullable String getVirtualHost() { return this.virtualHost; } /** * Configure a TCP client for managing TCP connections to the STOMP broker. - *

    By default {@link ReactorNettyTcpClient} or - * {@link ReactorNetty2TcpClient} is used. + *

    By default {@link ReactorNettyTcpClient} is used. *

    Note: when this property is used, any * {@link #setRelayHost(String) host} or {@link #setRelayPort(int) port} * specified are effectively ignored. @@ -371,8 +355,7 @@ public void setTcpClient(@Nullable TcpOperations tcpClient) { * invoked and this method is invoked before the handler is started and * hence a default implementation initialized). */ - @Nullable - public TcpOperations getTcpClient() { + public @Nullable TcpOperations getTcpClient() { return this.tcpClient; } @@ -389,8 +372,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia /** * Return the configured header initializer. */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } @@ -429,8 +411,7 @@ public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { this.taskScheduler = taskScheduler; } - @Nullable - public TaskScheduler getTaskScheduler() { + public @Nullable TaskScheduler getTaskScheduler() { return this.taskScheduler; } @@ -475,19 +456,10 @@ private TcpOperations initTcpClient() { if (this.headerInitializer != null) { decoder.setHeaderInitializer(this.headerInitializer); } - if (reactorNettyClientPresent) { - ReactorNettyCodec codec = new StompReactorNettyCodec(decoder); - ReactorNettyTcpClient client = new ReactorNettyTcpClient<>(this.relayHost, this.relayPort, codec); - client.setLogger(SimpLogging.forLog(client.getLogger())); - return client; - } - else if (reactorNetty2ClientPresent) { - TcpMessageCodec codec = new StompTcpMessageCodec(decoder); - ReactorNetty2TcpClient client = new ReactorNetty2TcpClient<>(this.relayHost, this.relayPort, codec); - client.setLogger(SimpLogging.forLog(client.getLogger())); - return client; - } - throw new IllegalStateException("No compatible version of Reactor Netty"); + ReactorNettyCodec codec = new StompReactorNettyCodec(decoder); + ReactorNettyTcpClient client = new ReactorNettyTcpClient<>(this.relayHost, this.relayPort, codec); + client.setLogger(SimpLogging.forLog(client.getLogger())); + return client; } @Override @@ -644,15 +616,13 @@ private class RelayConnectionHandler implements StompTcpConnectionHandler tcpConnection; + private volatile @Nullable TcpConnection tcpConnection; private volatile boolean isStompConnected; private long clientSendInterval; - @Nullable - private final AtomicInteger clientSendMessageCount; + private final @Nullable AtomicInteger clientSendMessageCount; private long clientSendMessageTimestamp; @@ -691,8 +661,7 @@ public StompHeaderAccessor getConnectHeaders() { return this.connectHeaders; } - @Nullable - protected TcpConnection getTcpConnection() { + protected @Nullable TcpConnection getTcpConnection() { return this.tcpConnection; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java index 03df60b74675..f699ea41bca5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompClientSupport.java @@ -19,7 +19,8 @@ import java.util.Arrays; import java.util.concurrent.TimeUnit; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.converter.MessageConverter; import org.springframework.messaging.converter.SimpleMessageConverter; import org.springframework.scheduling.TaskScheduler; @@ -44,8 +45,7 @@ public abstract class StompClientSupport { private MessageConverter messageConverter = new SimpleMessageConverter(); - @Nullable - private TaskScheduler taskScheduler; + private @Nullable TaskScheduler taskScheduler; private long[] defaultHeartbeat = new long[] {10000, 10000}; @@ -85,8 +85,7 @@ public void setTaskScheduler(@Nullable TaskScheduler taskScheduler) { /** * The configured TaskScheduler. */ - @Nullable - public TaskScheduler getTaskScheduler() { + public @Nullable TaskScheduler getTaskScheduler() { return this.taskScheduler; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java index 442c4301acc4..3e36da4a73e4 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompDecoder.java @@ -23,8 +23,8 @@ import java.util.List; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpLogging; import org.springframework.messaging.support.MessageBuilder; @@ -53,8 +53,7 @@ public class StompDecoder { private static final Log logger = SimpLogging.forLogName(StompDecoder.class); - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; /** @@ -68,8 +67,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia /** * Return the configured {@code MessageHeaderInitializer}, if any. */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } @@ -129,9 +127,8 @@ public List> decode(ByteBuffer byteBuffer, /** * Decode a single STOMP frame from the given {@code byteBuffer} into a {@link Message}. */ - @Nullable - @SuppressWarnings("NullAway") - private Message decodeMessage(ByteBuffer byteBuffer, @Nullable MultiValueMap headers) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + private @Nullable Message decodeMessage(ByteBuffer byteBuffer, @Nullable MultiValueMap headers) { Message decodedMessage = null; skipEol(byteBuffer); byteBuffer.mark(); @@ -302,8 +299,7 @@ else if (c == '\\') { return sb.toString(); } - @Nullable - private byte[] readPayload(ByteBuffer byteBuffer, StompHeaderAccessor headerAccessor) { + private byte @Nullable [] readPayload(ByteBuffer byteBuffer, StompHeaderAccessor headerAccessor) { Integer contentLength; try { contentLength = headerAccessor.getContentLength(); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java index 8d9da03c4252..a9b284fb58e9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompEncoder.java @@ -26,8 +26,8 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpLogging; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; @@ -126,8 +126,8 @@ private void writeHeaders( return; } - boolean shouldEscape = (command != StompCommand.CONNECT && command != StompCommand.STOMP - && command != StompCommand.CONNECTED); + boolean shouldEscape = (command != StompCommand.CONNECT && command != StompCommand.STOMP && + command != StompCommand.CONNECTED); for (Entry> entry : nativeHeaders.entrySet()) { if (command.requiresContentLength() && "content-length".equals(entry.getKey())) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java index a0a9448e1080..037e305fbfeb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompFrameHandler.java @@ -18,7 +18,7 @@ import java.lang.reflect.Type; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Contract to handle a STOMP frame. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java index 44fec8110a4f..03cc9b1fe616 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaderAccessor.java @@ -26,7 +26,8 @@ import java.util.Set; import java.util.concurrent.atomic.AtomicLong; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; import org.springframework.messaging.simp.SimpMessageType; @@ -185,8 +186,7 @@ protected MessageHeaderAccessor createAccessor(Message message) { // Redeclared for visibility within simp.stomp @Override - @Nullable - protected Map> getNativeHeaders() { + protected @Nullable Map> getNativeHeaders() { return super.getNativeHeaders(); } @@ -228,8 +228,7 @@ else if (!StompCommand.MESSAGE.equals(command)) { /** * Return the STOMP command, or {@code null} if not yet set. */ - @Nullable - public StompCommand getCommand() { + public @Nullable StompCommand getCommand() { return (StompCommand) getHeader(COMMAND_HEADER); } @@ -237,7 +236,7 @@ public boolean isHeartbeat() { return (SimpMessageType.HEARTBEAT == getMessageType()); } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public long[] getHeartbeat() { String rawValue = getFirstNativeHeader(STOMP_HEARTBEAT_HEADER); int pos = (rawValue != null ? rawValue.indexOf(',') : -1); @@ -261,8 +260,7 @@ public void setHost(@Nullable String host) { setNativeHeader(STOMP_HOST_HEADER, host); } - @Nullable - public String getHost() { + public @Nullable String getHost() { return getFirstNativeHeader(STOMP_HOST_HEADER); } @@ -300,8 +298,7 @@ private void trySetStompHeaderForSubscriptionId() { } } - @Nullable - public Integer getContentLength() { + public @Nullable Integer getContentLength() { String header = getFirstNativeHeader(STOMP_CONTENT_LENGTH_HEADER); return (header != null ? Integer.valueOf(header) : null); } @@ -318,8 +315,7 @@ public void setAck(@Nullable String ack) { setNativeHeader(STOMP_ACK_HEADER, ack); } - @Nullable - public String getAck() { + public @Nullable String getAck() { return getFirstNativeHeader(STOMP_ACK_HEADER); } @@ -327,8 +323,7 @@ public void setNack(@Nullable String nack) { setNativeHeader(STOMP_NACK_HEADER, nack); } - @Nullable - public String getNack() { + public @Nullable String getNack() { return getFirstNativeHeader(STOMP_NACK_HEADER); } @@ -336,8 +331,7 @@ public void setLogin(@Nullable String login) { setNativeHeader(STOMP_LOGIN_HEADER, login); } - @Nullable - public String getLogin() { + public @Nullable String getLogin() { return getFirstNativeHeader(STOMP_LOGIN_HEADER); } @@ -357,8 +351,7 @@ private void protectPasscode() { /** * Return the passcode header value, or {@code null} if not set. */ - @Nullable - public String getPasscode() { + public @Nullable String getPasscode() { StompPasscode credentials = (StompPasscode) getHeader(CREDENTIALS_HEADER); return (credentials != null ? credentials.passcode : null); } @@ -367,8 +360,7 @@ public void setReceiptId(@Nullable String receiptId) { setNativeHeader(STOMP_RECEIPT_ID_HEADER, receiptId); } - @Nullable - public String getReceiptId() { + public @Nullable String getReceiptId() { return getFirstNativeHeader(STOMP_RECEIPT_ID_HEADER); } @@ -376,13 +368,11 @@ public void setReceipt(@Nullable String receiptId) { setNativeHeader(STOMP_RECEIPT_HEADER, receiptId); } - @Nullable - public String getReceipt() { + public @Nullable String getReceipt() { return getFirstNativeHeader(STOMP_RECEIPT_HEADER); } - @Nullable - public String getMessage() { + public @Nullable String getMessage() { return getFirstNativeHeader(STOMP_MESSAGE_HEADER); } @@ -390,8 +380,7 @@ public void setMessage(@Nullable String content) { setNativeHeader(STOMP_MESSAGE_HEADER, content); } - @Nullable - public String getMessageId() { + public @Nullable String getMessageId() { return getFirstNativeHeader(STOMP_MESSAGE_ID_HEADER); } @@ -399,8 +388,7 @@ public void setMessageId(@Nullable String id) { setNativeHeader(STOMP_MESSAGE_ID_HEADER, id); } - @Nullable - public String getVersion() { + public @Nullable String getVersion() { return getFirstNativeHeader(STOMP_VERSION_HEADER); } @@ -526,22 +514,19 @@ public static StompHeaderAccessor wrap(Message message) { /** * Return the STOMP command from the given headers, or {@code null} if not set. */ - @Nullable - public static StompCommand getCommand(Map headers) { + public static @Nullable StompCommand getCommand(Map headers) { return (StompCommand) headers.get(COMMAND_HEADER); } /** * Return the passcode header value, or {@code null} if not set. */ - @Nullable - public static String getPasscode(Map headers) { + public static @Nullable String getPasscode(Map headers) { StompPasscode credentials = (StompPasscode) headers.get(CREDENTIALS_HEADER); return (credentials != null ? credentials.passcode : null); } - @Nullable - public static Integer getContentLength(Map> nativeHeaders) { + public static @Nullable Integer getContentLength(Map> nativeHeaders) { List values = nativeHeaders.get(STOMP_CONTENT_LENGTH_HEADER); return (!CollectionUtils.isEmpty(values) ? Integer.valueOf(values.get(0)) : null); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java index ef253caa5745..e39a580293cf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompHeaders.java @@ -26,7 +26,8 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -146,8 +147,7 @@ public void setContentType(@Nullable MimeType mimeType) { /** * Return the content-type header value. */ - @Nullable - public MimeType getContentType() { + public @Nullable MimeType getContentType() { String value = getFirst(CONTENT_TYPE); return (StringUtils.hasLength(value) ? MimeTypeUtils.parseMimeType(value) : null); } @@ -179,8 +179,7 @@ public void setReceipt(@Nullable String receipt) { /** * Get the receipt header. */ - @Nullable - public String getReceipt() { + public @Nullable String getReceipt() { return getFirst(RECEIPT); } @@ -195,8 +194,7 @@ public void setHost(@Nullable String host) { /** * Get the host header. */ - @Nullable - public String getHost() { + public @Nullable String getHost() { return getFirst(HOST); } @@ -205,7 +203,7 @@ public String getHost() { * Applies to the CONNECT frame. * @since 5.0.7 */ - public void setAcceptVersion(@Nullable String... acceptVersions) { + public void setAcceptVersion(String @Nullable ... acceptVersions) { if (ObjectUtils.isEmpty(acceptVersions)) { set(ACCEPT_VERSION, null); return; @@ -220,8 +218,7 @@ public void setAcceptVersion(@Nullable String... acceptVersions) { * Get the accept-version header. * @since 5.0.7 */ - @Nullable - public String[] getAcceptVersion() { + public String @Nullable [] getAcceptVersion() { String value = getFirst(ACCEPT_VERSION); return value != null ? StringUtils.commaDelimitedListToStringArray(value) : null; } @@ -237,8 +234,7 @@ public void setLogin(@Nullable String login) { /** * Get the login header. */ - @Nullable - public String getLogin() { + public @Nullable String getLogin() { return getFirst(LOGIN); } @@ -253,8 +249,7 @@ public void setPasscode(@Nullable String passcode) { /** * Get the passcode header. */ - @Nullable - public String getPasscode() { + public @Nullable String getPasscode() { return getFirst(PASSCODE); } @@ -262,7 +257,7 @@ public String getPasscode() { * Set the heartbeat header. * Applies to the CONNECT and CONNECTED frames. */ - public void setHeartbeat(@Nullable long[] heartbeat) { + public void setHeartbeat(long @Nullable [] heartbeat) { if (heartbeat == null || heartbeat.length != 2) { throw new IllegalArgumentException("Heart-beat array must be of length 2, not " + (heartbeat != null ? heartbeat.length : "null")); @@ -277,9 +272,8 @@ public void setHeartbeat(@Nullable long[] heartbeat) { /** * Get the heartbeat header. */ - @Nullable - @SuppressWarnings("NullAway") - public long[] getHeartbeat() { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public long @Nullable [] getHeartbeat() { String rawValue = getFirst(HEARTBEAT); int pos = (rawValue != null ? rawValue.indexOf(',') : -1); if (pos == -1) { @@ -309,8 +303,7 @@ public void setSession(@Nullable String session) { /** * Get the session header. */ - @Nullable - public String getSession() { + public @Nullable String getSession() { return getFirst(SESSION); } @@ -326,8 +319,7 @@ public void setServer(@Nullable String server) { * Get the server header. * Applies to the CONNECTED frame. */ - @Nullable - public String getServer() { + public @Nullable String getServer() { return getFirst(SERVER); } @@ -342,8 +334,7 @@ public void setDestination(@Nullable String destination) { * Get the destination header. * Applies to the SEND, SUBSCRIBE, and MESSAGE frames. */ - @Nullable - public String getDestination() { + public @Nullable String getDestination() { return getFirst(DESTINATION); } @@ -358,8 +349,7 @@ public void setId(@Nullable String id) { /** * Get the id header. */ - @Nullable - public String getId() { + public @Nullable String getId() { return getFirst(ID); } @@ -374,8 +364,7 @@ public void setAck(@Nullable String ack) { /** * Get the ack header. */ - @Nullable - public String getAck() { + public @Nullable String getAck() { return getFirst(ACK); } @@ -390,8 +379,7 @@ public void setSubscription(@Nullable String subscription) { /** * Get the subscription header. */ - @Nullable - public String getSubscription() { + public @Nullable String getSubscription() { return getFirst(SUBSCRIPTION); } @@ -406,8 +394,7 @@ public void setMessageId(@Nullable String messageId) { /** * Get the message-id header. */ - @Nullable - public String getMessageId() { + public @Nullable String getMessageId() { return getFirst(MESSAGE_ID); } @@ -422,8 +409,7 @@ public void setReceiptId(@Nullable String receiptId) { /** * Get the receipt header. */ - @Nullable - public String getReceiptId() { + public @Nullable String getReceiptId() { return getFirst(RECEIPT_ID); } @@ -433,8 +419,7 @@ public String getReceiptId() { * @return the first header value, or {@code null} if none */ @Override - @Nullable - public String getFirst(String headerName) { + public @Nullable String getFirst(String headerName) { List headerValues = this.headers.get(headerName); return headerValues != null ? headerValues.get(0) : null; } @@ -515,8 +500,7 @@ public boolean containsValue(Object value) { } @Override - @Nullable - public List get(Object key) { + public @Nullable List get(Object key) { return this.headers.get(key); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java index 94875b6d0a97..1af02d99ede0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSession.java @@ -18,7 +18,7 @@ import java.util.function.Consumer; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents a STOMP session with operations to send messages, @@ -136,8 +136,7 @@ interface Receiptable { * Return the receipt id, or {@code null} if the STOMP frame for which * the handle was returned did not have a "receipt" header. */ - @Nullable - String getReceiptId(); + @Nullable String getReceiptId(); /** * Task to invoke when a receipt is received. @@ -173,8 +172,7 @@ interface Subscription extends Receiptable { /** * Return the id for the subscription. */ - @Nullable - String getSubscriptionId(); + @Nullable String getSubscriptionId(); /** * Return the headers used on the SUBSCRIBE frame. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java index c55b8b26f5d7..aefd083cce5b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandler.java @@ -16,7 +16,7 @@ package org.springframework.messaging.simp.stomp; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A contract for client STOMP session lifecycle events including a callback diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java index 2d57f14574b0..cc0b13afb99b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/StompSessionHandlerAdapter.java @@ -18,7 +18,7 @@ import java.lang.reflect.Type; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Abstract adapter class for {@link StompSessionHandler} with mostly empty diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/package-info.java index 6b42fa754c88..d4b398bc07a7 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/stomp/package-info.java @@ -1,9 +1,7 @@ /** * Generic support for simple messaging protocols (like STOMP). */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp.stomp; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java index 822864ae09b4..f059866110d5 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/DefaultUserDestinationResolver.java @@ -22,8 +22,8 @@ import java.util.Set; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.simp.SimpLogging; @@ -124,8 +124,7 @@ public boolean isRemoveLeadingSlash() { @Override - @Nullable - public UserDestinationResult resolveDestination(Message message) { + public @Nullable UserDestinationResult resolveDestination(Message message) { ParseResult parseResult = parse(message); if (parseResult == null) { return null; @@ -145,8 +144,7 @@ public UserDestinationResult resolveDestination(Message message) { return new UserDestinationResult(sourceDest, targetSet, subscribeDest, user, sessionIds); } - @Nullable - private ParseResult parse(Message message) { + private @Nullable ParseResult parse(Message message) { MessageHeaders headers = message.getHeaders(); String sourceDestination = SimpMessageHeaderAccessor.getDestination(headers); if (sourceDestination == null || !checkDestination(sourceDestination, this.prefix)) { @@ -163,8 +161,7 @@ private ParseResult parse(Message message) { return null; } - @Nullable - private ParseResult parseSubscriptionMessage(Message message, String sourceDestination) { + private @Nullable ParseResult parseSubscriptionMessage(Message message, String sourceDestination) { MessageHeaders headers = message.getHeaders(); String sessionId = SimpMessageHeaderAccessor.getSessionId(headers); if (sessionId == null) { @@ -243,8 +240,7 @@ protected boolean checkDestination(String destination, String requiredPrefix) { * @return a target destination, or {@code null} if none */ @SuppressWarnings("unused") - @Nullable - protected String getTargetDestination(String sourceDestination, String actualDestination, + protected @Nullable String getTargetDestination(String sourceDestination, String actualDestination, String sessionId, @Nullable String user) { return actualDestination + "-user" + sessionId; @@ -269,8 +265,7 @@ private static class ParseResult { private final Set sessionIds; - @Nullable - private final String user; + private final @Nullable String user; public ParseResult(String sourceDest, String actualDest, String subscribeDest, Set sessionIds, @Nullable String user) { @@ -313,8 +308,7 @@ public Set getSessionIds() { /** * The name of the user associated with the session. */ - @Nullable - public String getUser() { + public @Nullable String getUser() { return this.user; } } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java index 14d4c21df46f..f041968b5222 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/MultiServerUserRegistry.java @@ -28,10 +28,11 @@ import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationEvent; import org.springframework.context.event.SmartApplicationListener; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.converter.MessageConverter; import org.springframework.util.Assert; @@ -117,8 +118,7 @@ public void onApplicationEvent(ApplicationEvent event) { // SimpUserRegistry methods @Override - @Nullable - public SimpUser getUser(String userName) { + public @Nullable SimpUser getUser(String userName) { // Prefer remote registries due to cross-server SessionLookup for (UserRegistrySnapshot registry : this.remoteRegistries.values()) { SimpUser user = registry.getUserMap().get(userName); @@ -279,8 +279,7 @@ private static class TransferSimpUser implements SimpUser { private final Set sessions; // Cross-server session lookup (for example, user connected to multiple servers) - @Nullable - private SessionLookup sessionLookup; + private @Nullable SessionLookup sessionLookup; /** * Default constructor for JSON deserialization. @@ -312,9 +311,8 @@ public String getName() { return this.name; } - @Nullable @Override - public Principal getPrincipal() { + public @Nullable Principal getPrincipal() { return null; } @@ -327,8 +325,7 @@ public boolean hasSessions() { } @Override - @Nullable - public SimpSession getSession(String sessionId) { + public @Nullable SimpSession getSession(String sessionId) { if (this.sessionLookup != null) { return this.sessionLookup.findSessions(getName()).get(sessionId); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUser.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUser.java index bcd0d6739273..677d467d5bfa 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUser.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUser.java @@ -19,7 +19,7 @@ import java.security.Principal; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents a connected user. @@ -40,8 +40,7 @@ public interface SimpUser { * server in a multi-server user registry scenario. * @since 5.3 */ - @Nullable - Principal getPrincipal(); + @Nullable Principal getPrincipal(); /** * Whether the user has any sessions. @@ -53,8 +52,7 @@ public interface SimpUser { * @param sessionId the session id * @return the matching session, or {@code null} if none found */ - @Nullable - SimpSession getSession(String sessionId); + @Nullable SimpSession getSession(String sessionId); /** * Return the sessions for the user. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUserRegistry.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUserRegistry.java index 9847104d31cb..ded0e4d22145 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUserRegistry.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/SimpUserRegistry.java @@ -18,7 +18,7 @@ import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A registry of currently connected users. @@ -33,8 +33,7 @@ public interface SimpUserRegistry { * @param userName the name of the user to look up * @return the user, or {@code null} if not connected */ - @Nullable - SimpUser getUser(String userName); + @Nullable SimpUser getUser(String userName); /** * Return a snapshot of all connected users. diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java index 791602c0c1fd..9accb2c67999 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationMessageHandler.java @@ -23,9 +23,9 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; +import org.jspecify.annotations.Nullable; import org.springframework.context.SmartLifecycle; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -68,16 +68,13 @@ public class UserDestinationMessageHandler implements MessageHandler, SmartLifec private final SendHelper sendHelper; - @Nullable - private BroadcastHandler broadcastHandler; + private @Nullable BroadcastHandler broadcastHandler; - @Nullable - private MessageHeaderInitializer headerInitializer; + private @Nullable MessageHeaderInitializer headerInitializer; private volatile boolean running; - @Nullable - private Integer phase; + private @Nullable Integer phase; private final Object lifecycleMonitor = new Object(); @@ -126,8 +123,7 @@ public void setBroadcastDestination(@Nullable String destination) { /** * Return the configured destination for unresolved messages. */ - @Nullable - public String getBroadcastDestination() { + public @Nullable String getBroadcastDestination() { return (this.broadcastHandler != null ? this.broadcastHandler.getBroadcastDestination() : null); } @@ -151,8 +147,7 @@ public void setHeaderInitializer(@Nullable MessageHeaderInitializer headerInitia /** * Return the configured header initializer. */ - @Nullable - public MessageHeaderInitializer getHeaderInitializer() { + public @Nullable MessageHeaderInitializer getHeaderInitializer() { return this.headerInitializer; } @@ -261,8 +256,7 @@ private static class SendHelper { private final MessageSendingOperations messagingTemplate; - @Nullable - private final Map> orderedMessagingTemplates; + private final @Nullable Map> orderedMessagingTemplates; SendHelper(MessageChannel clientInboundChannel, MessageChannel brokerChannel) { this.brokerChannel = brokerChannel; @@ -331,8 +325,7 @@ public String getBroadcastDestination() { return this.broadcastDestination; } - @Nullable - public Message preHandle(Message message) throws MessagingException { + public @Nullable Message preHandle(Message message) throws MessagingException { String destination = SimpMessageHeaderAccessor.getDestination(message.getHeaders()); if (!getBroadcastDestination().equals(destination)) { return message; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResolver.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResolver.java index 95e5e4480874..1efb0315a644 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResolver.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResolver.java @@ -16,7 +16,8 @@ package org.springframework.messaging.simp.user; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; /** @@ -45,7 +46,6 @@ public interface UserDestinationResolver { * @return 0 or more target messages (one for each active session), or * {@code null} if the source message does not contain a user destination. */ - @Nullable - UserDestinationResult resolveDestination(Message message); + @Nullable UserDestinationResult resolveDestination(Message message); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java index 2abcbe93e0a0..b6190c6ebcc1 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserDestinationResult.java @@ -19,7 +19,8 @@ import java.util.Collections; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -38,8 +39,7 @@ public class UserDestinationResult { private final String subscribeDestination; - @Nullable - private final String user; + private final @Nullable String user; private final Set sessionIds; @@ -110,8 +110,7 @@ public String getSubscribeDestination() { * sessionId in place of a user name thus removing the need for a user-to-session * lookup via {@link SimpUserRegistry}. */ - @Nullable - public String getUser() { + public @Nullable String getUser() { return this.user; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserRegistryMessageHandler.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserRegistryMessageHandler.java index eb55f0110e40..4696e3049d9d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserRegistryMessageHandler.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/UserRegistryMessageHandler.java @@ -20,8 +20,9 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationListener; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHandler; import org.springframework.messaging.MessagingException; @@ -55,8 +56,7 @@ public class UserRegistryMessageHandler implements MessageHandler, ApplicationLi private final UserRegistryTask schedulerTask = new UserRegistryTask(); - @Nullable - private volatile ScheduledFuture scheduledFuture; + private volatile @Nullable ScheduledFuture scheduledFuture; private long registryExpirationPeriod = TimeUnit.SECONDS.toMillis(20); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/package-info.java index bd59b706f119..95d5b632c1b2 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/simp/user/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/simp/user/package-info.java @@ -6,9 +6,7 @@ *

    Also included is {@link org.springframework.messaging.simp.user.SimpUserRegistry} * for keeping track of connected user sessions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.simp.user; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractHeaderMapper.java b/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractHeaderMapper.java index c729f871f382..2405bcdf0ea9 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractHeaderMapper.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractHeaderMapper.java @@ -20,8 +20,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.messaging.MessageHeaders; import org.springframework.util.StringUtils; @@ -90,8 +90,7 @@ protected String toHeaderName(String propertyName) { * Return the header value, or {@code null} if it does not exist * or does not match the requested {@code type}. */ - @Nullable - protected V getHeaderIfAvailable(Map headers, String name, Class type) { + protected @Nullable V getHeaderIfAvailable(Map headers, String name, Class type) { Object value = headers.get(name); if (value == null) { return null; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java b/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java index d1d209e8af64..6c097e581429 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/AbstractMessageChannel.java @@ -22,9 +22,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanNameAware; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageDeliveryException; @@ -174,8 +174,7 @@ protected class ChannelInterceptorChain { private int receiveInterceptorIndex = -1; - @Nullable - public Message applyPreSend(Message message, MessageChannel channel) { + public @Nullable Message applyPreSend(Message message, MessageChannel channel) { Message messageToUse = message; for (ChannelInterceptor interceptor : interceptors) { Message resolvedMessage = interceptor.preSend(messageToUse, channel); @@ -224,8 +223,7 @@ public boolean applyPreReceive(MessageChannel channel) { return true; } - @Nullable - public Message applyPostReceive(Message message, MessageChannel channel) { + public @Nullable Message applyPostReceive(Message message, MessageChannel channel) { Message messageToUse = message; for (ChannelInterceptor interceptor : interceptors) { messageToUse = interceptor.postReceive(messageToUse, channel); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptor.java index 4dd9dbd7c2df..b83c54e369cf 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/ChannelInterceptor.java @@ -16,7 +16,8 @@ package org.springframework.messaging.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; @@ -39,8 +40,7 @@ public interface ChannelInterceptor { * If this method returns {@code null} then the actual * send invocation will not occur. */ - @Nullable - default Message preSend(Message message, MessageChannel channel) { + default @Nullable Message preSend(Message message, MessageChannel channel) { return message; } @@ -77,8 +77,7 @@ default boolean preReceive(MessageChannel channel) { * necessary; {@code null} aborts further interceptor invocations. * This only applies to PollableChannels. */ - @Nullable - default Message postReceive(Message message, MessageChannel channel) { + default @Nullable Message postReceive(Message message, MessageChannel channel) { return message; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java b/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java index e097701169eb..d0b31f6adcc0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/ErrorMessage.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; @@ -45,9 +46,8 @@ public class ErrorMessage extends GenericMessage { private static final long serialVersionUID = -5470210965279837728L; - @Nullable @SuppressWarnings("serial") - private final Message originalMessage; + private final @Nullable Message originalMessage; /** @@ -129,8 +129,7 @@ public ErrorMessage(Throwable payload, MessageHeaders headers, Message origin * where the ErrorMessage was created. * @since 5.0 */ - @Nullable - public Message getOriginalMessage() { + public @Nullable Message getOriginalMessage() { return this.originalMessage; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorChannelInterceptor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorChannelInterceptor.java index ef34ae90ec65..57d92d29be0d 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorChannelInterceptor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorChannelInterceptor.java @@ -16,7 +16,8 @@ package org.springframework.messaging.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -46,8 +47,7 @@ public interface ExecutorChannelInterceptor extends ChannelInterceptor { * @param handler the target handler to handle the message * @return the input message, or a new instance, or {@code null} */ - @Nullable - default Message beforeHandle(Message message, MessageChannel channel, MessageHandler handler) { + default @Nullable Message beforeHandle(Message message, MessageChannel channel, MessageHandler handler) { return message; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorSubscribableChannel.java b/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorSubscribableChannel.java index 1d8b56531d52..8875e6ba9f6c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorSubscribableChannel.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/ExecutorSubscribableChannel.java @@ -21,7 +21,8 @@ import java.util.concurrent.Executor; import java.util.concurrent.RejectedExecutionException; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageDeliveryException; import org.springframework.messaging.MessageHandler; @@ -37,8 +38,7 @@ */ public class ExecutorSubscribableChannel extends AbstractSubscribableChannel { - @Nullable - private final Executor executor; + private final @Nullable Executor executor; private final List executorInterceptors = new ArrayList<>(4); @@ -62,8 +62,7 @@ public ExecutorSubscribableChannel(@Nullable Executor executor) { } - @Nullable - public Executor getExecutor() { + public @Nullable Executor getExecutor() { return this.executor; } @@ -168,8 +167,7 @@ public void run() { } } - @Nullable - private Message applyBeforeHandle(Message message) { + private @Nullable Message applyBeforeHandle(Message message) { Message messageToUse = message; for (ExecutorChannelInterceptor interceptor : executorInterceptors) { messageToUse = interceptor.beforeHandle(messageToUse, ExecutorSubscribableChannel.this, this.messageHandler); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java b/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java index 72b06f560ad4..263f1660229a 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/GenericMessage.java @@ -19,7 +19,8 @@ import java.io.Serializable; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.util.Assert; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/IdTimestampMessageHeaderInitializer.java b/spring-messaging/src/main/java/org/springframework/messaging/support/IdTimestampMessageHeaderInitializer.java index cbd338cda89b..9c05c25d24c0 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/IdTimestampMessageHeaderInitializer.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/IdTimestampMessageHeaderInitializer.java @@ -16,7 +16,8 @@ package org.springframework.messaging.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.MessageHeaders; import org.springframework.util.IdGenerator; @@ -32,8 +33,7 @@ public class IdTimestampMessageHeaderInitializer implements MessageHeaderInitial private static final IdGenerator ID_VALUE_NONE_GENERATOR = () -> MessageHeaders.ID_VALUE_NONE; - @Nullable - private IdGenerator idGenerator; + private @Nullable IdGenerator idGenerator; private boolean enableTimestamp; @@ -52,8 +52,7 @@ public void setIdGenerator(@Nullable IdGenerator idGenerator) { /** * Return the configured {@code IdGenerator}, if any. */ - @Nullable - public IdGenerator getIdGenerator() { + public @Nullable IdGenerator getIdGenerator() { return this.idGenerator; } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java index 028bbbc52470..73cd088c7500 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/MessageBuilder.java @@ -18,7 +18,8 @@ import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; @@ -41,8 +42,7 @@ public final class MessageBuilder { private final T payload; - @Nullable - private final Message providedMessage; + private final @Nullable Message providedMessage; private MessageHeaderAccessor headerAccessor; 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 ab39f57211b8..093f9f9667c6 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. @@ -26,7 +26,8 @@ import java.util.Map; import java.util.UUID; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; @@ -120,8 +121,7 @@ public class MessageHeaderAccessor { private boolean enableTimestamp = false; - @Nullable - private IdGenerator idGenerator; + private @Nullable IdGenerator idGenerator; private MessageHeaderAccessor(@Nullable MessageHeaders headers) { this.headers = new MutableMessageHeaders(headers); @@ -299,8 +299,7 @@ public Map toMap() { * @param headerName the name of the header * @return the associated value, or {@code null} if none found */ - @Nullable - public Object getHeader(String headerName) { + public @Nullable Object getHeader(String headerName) { return this.headers.get(headerName); } @@ -332,7 +331,7 @@ public void setHeader(String name, @Nullable Object value) { 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"); @@ -433,8 +432,7 @@ protected boolean isReadOnly(String headerName) { // Specific header accessors - @Nullable - public UUID getId() { + public @Nullable UUID getId() { Object value = getHeader(MessageHeaders.ID); if (value == null) { return null; @@ -442,8 +440,7 @@ public UUID getId() { return (value instanceof UUID uuid ? uuid : UUID.fromString(value.toString())); } - @Nullable - public Long getTimestamp() { + public @Nullable Long getTimestamp() { Object value = getHeader(MessageHeaders.TIMESTAMP); if (value == null) { return null; @@ -455,8 +452,7 @@ public void setContentType(MimeType contentType) { setHeader(MessageHeaders.CONTENT_TYPE, contentType); } - @Nullable - public MimeType getContentType() { + public @Nullable MimeType getContentType() { Object value = getHeader(MessageHeaders.CONTENT_TYPE); if (value == null) { return null; @@ -478,8 +474,7 @@ public void setReplyChannel(MessageChannel replyChannel) { setHeader(MessageHeaders.REPLY_CHANNEL, replyChannel); } - @Nullable - public Object getReplyChannel() { + public @Nullable Object getReplyChannel() { return getHeader(MessageHeaders.REPLY_CHANNEL); } @@ -491,8 +486,7 @@ public void setErrorChannel(MessageChannel errorChannel) { setHeader(MessageHeaders.ERROR_CHANNEL, errorChannel); } - @Nullable - public Object getErrorChannel() { + public @Nullable Object getErrorChannel() { return getHeader(MessageHeaders.ERROR_CHANNEL); } @@ -586,8 +580,7 @@ public String toString() { * @return an accessor instance of the specified type, or {@code null} if none * @since 5.1.19 */ - @Nullable - public static MessageHeaderAccessor getAccessor(Message message) { + public static @Nullable MessageHeaderAccessor getAccessor(Message message) { return getAccessor(message.getHeaders(), null); } @@ -602,8 +595,7 @@ public static MessageHeaderAccessor getAccessor(Message message) { * @return an accessor instance of the specified type, or {@code null} if none * @since 4.1 */ - @Nullable - public static T getAccessor(Message message, @Nullable Class requiredType) { + public static @Nullable T getAccessor(Message message, @Nullable Class requiredType) { return getAccessor(message.getHeaders(), requiredType); } @@ -617,8 +609,7 @@ public static T getAccessor(Message message * @since 4.1 */ @SuppressWarnings("unchecked") - @Nullable - public static T getAccessor( + public static @Nullable T getAccessor( MessageHeaders messageHeaders, @Nullable Class requiredType) { if (messageHeaders instanceof MutableMessageHeaders mutableHeaders) { diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java index aafc1dcca400..e8ab4c98883b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/NativeMessageHeaderAccessor.java @@ -21,7 +21,8 @@ import java.util.List; import java.util.Map; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.messaging.Message; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -87,8 +88,7 @@ protected NativeMessageHeaderAccessor(@Nullable Message message) { * Subclasses can use this method to access the "native" headers sub-map. */ @SuppressWarnings("unchecked") - @Nullable - protected Map> getNativeHeaders() { + protected @Nullable Map> getNativeHeaders() { return (Map>) getHeader(NATIVE_HEADERS); } @@ -158,8 +158,7 @@ public boolean containsNativeHeader(String headerName) { * @param headerName the name of the header * @return the associated values, or {@code null} if none */ - @Nullable - public List getNativeHeader(String headerName) { + public @Nullable List getNativeHeader(String headerName) { Map> map = getNativeHeaders(); return (map != null ? map.get(headerName) : null); } @@ -169,8 +168,7 @@ public List getNativeHeader(String headerName) { * @param headerName the name of the header * @return the associated value, or {@code null} if none */ - @Nullable - public String getFirstNativeHeader(String headerName) { + public @Nullable String getFirstNativeHeader(String headerName) { Map> map = getNativeHeaders(); if (map != null) { List values = map.get(headerName); @@ -272,8 +270,7 @@ public void addNativeHeaders(@Nullable MultiValueMap headers) { * @param headerName the name of the header * @return the associated values, or {@code null} if the header was not present */ - @Nullable - public List removeNativeHeader(String headerName) { + public @Nullable List removeNativeHeader(String headerName) { Assert.state(isMutable(), "Already immutable"); Map> nativeHeaders = getNativeHeaders(); if (CollectionUtils.isEmpty(nativeHeaders)) { @@ -291,8 +288,7 @@ public List removeNativeHeader(String headerName) { * @return the associated value, or {@code null} if none */ @SuppressWarnings("unchecked") - @Nullable - public static String getFirstNativeHeader(String headerName, Map headers) { + public static @Nullable String getFirstNativeHeader(String headerName, Map headers) { Map> map = (Map>) headers.get(NATIVE_HEADERS); if (map != null) { List values = map.get(headerName); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/support/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/support/package-info.java index b2213f6a1634..cdc809c8792b 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/support/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/support/package-info.java @@ -4,9 +4,7 @@ * message headers, as well as various {@link org.springframework.messaging.MessageChannel} * implementations and channel interceptor support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/ReconnectStrategy.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/ReconnectStrategy.java index 572866d39941..eebbc41975bb 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/ReconnectStrategy.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/ReconnectStrategy.java @@ -16,7 +16,7 @@ package org.springframework.messaging.tcp; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A contract to determine the frequency of reconnect attempts after connection failure. @@ -32,7 +32,6 @@ public interface ReconnectStrategy { * @param attemptCount how many reconnect attempts have been made already * @return the amount of time in milliseconds, or {@code null} to stop */ - @Nullable - Long getTimeToNextAttempt(int attemptCount); + @Nullable Long getTimeToNextAttempt(int attemptCount); } diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java index 545b1f3d1218..c3f1bdccca4c 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpConnection.java @@ -30,20 +30,6 @@ */ public interface TcpConnection

    extends Closeable { - /** - * Send the given message. - * @param message the message - * @return a ListenableFuture that can be used to determine when and if the - * message was successfully sent - * @deprecated as of 6.0, in favor of {@link #sendAsync(Message)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - default org.springframework.util.concurrent.ListenableFuture send(Message

    message) { - return new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>( - sendAsync(message)); - } - /** * Send the given message. * @param message the message diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java index 6ee521358da9..d6a7099ab667 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/TcpOperations.java @@ -27,22 +27,6 @@ */ public interface TcpOperations

    { - /** - * Open a new connection. - * @param connectionHandler a handler to manage the connection - * @return a ListenableFuture that can be used to determine when and if the - * connection is successfully established - * @deprecated as of 6.0, in favor of {@link #connectAsync(TcpConnectionHandler)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - default org.springframework.util.concurrent.ListenableFuture connect( - TcpConnectionHandler

    connectionHandler) { - - return new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>( - connectAsync(connectionHandler)); - } - /** * Open a new connection. * @param connectionHandler a handler to manage the connection @@ -52,23 +36,6 @@ default org.springframework.util.concurrent.ListenableFuture connect( */ CompletableFuture connectAsync(TcpConnectionHandler

    connectionHandler); - /** - * Open a new connection and a strategy for reconnecting if the connection fails. - * @param connectionHandler a handler to manage the connection - * @param reconnectStrategy a strategy for reconnecting - * @return a ListenableFuture that can be used to determine when and if the - * initial connection is successfully established - * @deprecated as of 6.0, in favor of {@link #connectAsync(TcpConnectionHandler, ReconnectStrategy)} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - default org.springframework.util.concurrent.ListenableFuture connect( - TcpConnectionHandler

    connectionHandler, ReconnectStrategy reconnectStrategy) { - - return new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>( - connectAsync(connectionHandler, reconnectStrategy)); - } - /** * Open a new connection and a strategy for reconnecting if the connection fails. * @param connectionHandler a handler to manage the connection @@ -79,18 +46,6 @@ default org.springframework.util.concurrent.ListenableFuture connect( */ CompletableFuture connectAsync(TcpConnectionHandler

    connectionHandler, ReconnectStrategy reconnectStrategy); - /** - * Shut down and close any open connections. - * @return a ListenableFuture that can be used to determine when and if the - * connection is successfully closed - * @deprecated as of 6.0, in favor of {@link #shutdownAsync()} - */ - @Deprecated(since = "6.0", forRemoval = true) - @SuppressWarnings("removal") - default org.springframework.util.concurrent.ListenableFuture shutdown() { - return new org.springframework.util.concurrent.CompletableToListenableFutureAdapter<>(shutdownAsync()); - } - /** * Shut down and close any open connections. * @return a CompletableFuture that can be used to determine when and if the diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/package-info.java index 2b940f2170cf..8eb670ba0051 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/package-info.java @@ -6,9 +6,7 @@ * as well as sending messages via * {@link org.springframework.messaging.tcp.TcpConnection TcpConnection}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.tcp; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNetty2TcpClient.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNetty2TcpClient.java deleted file mode 100644 index 624201cdcb2b..000000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNetty2TcpClient.java +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.tcp.reactor; - -import java.nio.ByteBuffer; -import java.time.Duration; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.function.BiFunction; -import java.util.function.Function; - -import io.netty5.buffer.Buffer; -import io.netty5.channel.ChannelHandlerContext; -import io.netty5.channel.group.ChannelGroup; -import io.netty5.channel.group.DefaultChannelGroup; -import io.netty5.handler.codec.ByteToMessageDecoder; -import io.netty5.util.concurrent.ImmediateEventExecutor; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.reactivestreams.Publisher; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.core.scheduler.Scheduler; -import reactor.core.scheduler.Schedulers; -import reactor.netty5.Connection; -import reactor.netty5.NettyInbound; -import reactor.netty5.NettyOutbound; -import reactor.netty5.resources.ConnectionProvider; -import reactor.netty5.resources.LoopResources; -import reactor.netty5.tcp.TcpClient; -import reactor.util.retry.Retry; - -import org.springframework.lang.Nullable; -import org.springframework.messaging.Message; -import org.springframework.messaging.tcp.ReconnectStrategy; -import org.springframework.messaging.tcp.TcpConnection; -import org.springframework.messaging.tcp.TcpConnectionHandler; -import org.springframework.messaging.tcp.TcpOperations; -import org.springframework.util.Assert; - -/** - * Reactor Netty based implementation of {@link TcpOperations}. - * - *

    This class is based on {@link ReactorNettyTcpClient}. - * - * @author Rossen Stoyanchev - * @since 6.0 - * @param

    the type of payload for in and outbound messages - */ -public class ReactorNetty2TcpClient

    implements TcpOperations

    { - - private static final int PUBLISH_ON_BUFFER_SIZE = 16; - - - private final TcpClient tcpClient; - - private final TcpMessageCodec

    codec; - - @Nullable - private final ChannelGroup channelGroup; - - @Nullable - private final LoopResources loopResources; - - @Nullable - private final ConnectionProvider poolResources; - - private final Scheduler scheduler = Schedulers.newParallel("tcp-client-scheduler"); - - private Log logger = LogFactory.getLog(ReactorNetty2TcpClient.class); - - private volatile boolean stopping; - - - /** - * Simple constructor with the host and port to use to connect to. - *

    This constructor manages the lifecycle of the {@link TcpClient} and - * underlying resources such as {@link ConnectionProvider}, - * {@link LoopResources}, and {@link ChannelGroup}. - *

    For full control over the initialization and lifecycle of the - * TcpClient, use {@link #ReactorNetty2TcpClient(TcpClient, TcpMessageCodec)}. - * @param host the host to connect to - * @param port the port to connect to - * @param codec for encoding and decoding the input/output byte streams - * @see org.springframework.messaging.simp.stomp.StompReactorNettyCodec - */ - public ReactorNetty2TcpClient(String host, int port, TcpMessageCodec

    codec) { - Assert.notNull(host, "host is required"); - Assert.notNull(codec, "ReactorNettyCodec is required"); - - this.channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); - this.loopResources = LoopResources.create("tcp-client-loop"); - this.poolResources = ConnectionProvider.create("tcp-client-pool", 10000); - this.codec = codec; - - this.tcpClient = TcpClient.create(this.poolResources) - .host(host).port(port) - .runOn(this.loopResources, false) - .doOnConnected(conn -> this.channelGroup.add(conn.channel())); - } - - /** - * A variant of {@link #ReactorNetty2TcpClient(String, int, TcpMessageCodec)} - * that still manages the lifecycle of the {@link TcpClient} and underlying - * resources, but allows for direct configuration of other properties of the - * client through a {@code Function}. - * @param clientConfigurer the configurer function - * @param codec for encoding and decoding the input/output byte streams - * @since 5.1.3 - * @see org.springframework.messaging.simp.stomp.StompReactorNettyCodec - */ - public ReactorNetty2TcpClient(Function clientConfigurer, TcpMessageCodec

    codec) { - Assert.notNull(codec, "ReactorNettyCodec is required"); - - this.channelGroup = new DefaultChannelGroup(ImmediateEventExecutor.INSTANCE); - this.loopResources = LoopResources.create("tcp-client-loop"); - this.poolResources = ConnectionProvider.create("tcp-client-pool", 10000); - this.codec = codec; - - this.tcpClient = clientConfigurer.apply(TcpClient - .create(this.poolResources) - .runOn(this.loopResources, false) - .doOnConnected(conn -> this.channelGroup.add(conn.channel()))); - } - - /** - * Constructor with an externally created {@link TcpClient} instance whose - * lifecycle is expected to be managed externally. - * @param tcpClient the TcpClient instance to use - * @param codec for encoding and decoding the input/output byte streams - * @see org.springframework.messaging.simp.stomp.StompReactorNettyCodec - */ - public ReactorNetty2TcpClient(TcpClient tcpClient, TcpMessageCodec

    codec) { - Assert.notNull(tcpClient, "TcpClient is required"); - Assert.notNull(codec, "ReactorNettyCodec is required"); - this.tcpClient = tcpClient; - this.codec = codec; - - this.channelGroup = null; - this.loopResources = null; - this.poolResources = null; - } - - - /** - * Set an alternative logger to use than the one based on the class name. - * @param logger the logger to use - * @since 5.1 - */ - public void setLogger(Log logger) { - this.logger = logger; - } - - /** - * Return the currently configured Logger. - * @since 5.1 - */ - public Log getLogger() { - return logger; - } - - - @Override - public CompletableFuture connectAsync(TcpConnectionHandler

    handler) { - Assert.notNull(handler, "TcpConnectionHandler is required"); - - if (this.stopping) { - return handleShuttingDownConnectFailure(handler); - } - - return extendTcpClient(this.tcpClient, handler) - .handle(new ReactorNettyHandler(handler)) - .connect() - .doOnError(handler::afterConnectFailure) - .then().toFuture(); - } - - /** - * Provides an opportunity to initialize the {@link TcpClient} for the given - * {@link TcpConnectionHandler} which may implement sub-interfaces such as - * {@link org.springframework.messaging.simp.stomp.StompTcpConnectionHandler} - * that expose further information. - * @param tcpClient the candidate TcpClient - * @param handler the handler for the TCP connection - * @return the same handler or an updated instance - */ - protected TcpClient extendTcpClient(TcpClient tcpClient, TcpConnectionHandler

    handler) { - return tcpClient; - } - - @Override - public CompletableFuture connectAsync(TcpConnectionHandler

    handler, ReconnectStrategy strategy) { - Assert.notNull(handler, "TcpConnectionHandler is required"); - Assert.notNull(strategy, "ReconnectStrategy is required"); - - if (this.stopping) { - return handleShuttingDownConnectFailure(handler); - } - - // Report first connect to the ListenableFuture - CompletableFuture connectFuture = new CompletableFuture<>(); - - extendTcpClient(this.tcpClient, handler) - .handle(new ReactorNettyHandler(handler)) - .connect() - .doOnNext(conn -> connectFuture.complete(null)) - .doOnError(connectFuture::completeExceptionally) - .doOnError(handler::afterConnectFailure) // report all connect failures to the handler - .flatMap(Connection::onDispose) // post-connect issues - .retryWhen(Retry.from(signals -> signals - .map(retrySignal -> (int) retrySignal.totalRetriesInARow()) - .flatMap(attempt -> reconnect(attempt, strategy)))) - .repeatWhen(flux -> flux - .scan(1, (count, element) -> count++) - .flatMap(attempt -> reconnect(attempt, strategy))) - .subscribe(); - return connectFuture; - } - - private CompletableFuture handleShuttingDownConnectFailure(TcpConnectionHandler

    handler) { - IllegalStateException ex = new IllegalStateException("Shutting down."); - handler.afterConnectFailure(ex); - return Mono.error(ex).toFuture(); - } - - private Publisher reconnect(Integer attempt, ReconnectStrategy reconnectStrategy) { - Long time = reconnectStrategy.getTimeToNextAttempt(attempt); - return (time != null ? Mono.delay(Duration.ofMillis(time), this.scheduler) : Mono.empty()); - } - - @Override - public CompletableFuture shutdownAsync() { - if (this.stopping) { - return CompletableFuture.completedFuture(null); - } - - this.stopping = true; - - Mono result; - if (this.channelGroup != null) { - Sinks.Empty channnelGroupCloseSink = Sinks.empty(); - this.channelGroup.close().addListener(future -> channnelGroupCloseSink.tryEmitEmpty()); - result = channnelGroupCloseSink.asMono(); - if (this.loopResources != null) { - result = result.onErrorComplete().then(this.loopResources.disposeLater()); - } - if (this.poolResources != null) { - result = result.onErrorComplete().then(this.poolResources.disposeLater()); - } - result = result.onErrorComplete().then(stopScheduler()); - } - else { - result = stopScheduler(); - } - - return result.toFuture(); - } - - private Mono stopScheduler() { - return Mono.fromRunnable(() -> { - this.scheduler.dispose(); - for (int i = 0; i < 20; i++) { - if (this.scheduler.isDisposed()) { - break; - } - try { - Thread.sleep(100); - } - catch (Throwable ex) { - break; - } - } - }); - } - - @Override - public String toString() { - return "ReactorNetty2TcpClient[" + this.tcpClient + "]"; - } - - - private class ReactorNettyHandler implements BiFunction> { - - private final TcpConnectionHandler

    connectionHandler; - - ReactorNettyHandler(TcpConnectionHandler

    handler) { - this.connectionHandler = handler; - } - - @Override - @SuppressWarnings("unchecked") - public Publisher apply(NettyInbound inbound, NettyOutbound outbound) { - inbound.withConnection(conn -> { - if (logger.isDebugEnabled()) { - logger.debug("Connected to " + conn.address()); - } - }); - Sinks.Empty completionSink = Sinks.empty(); - TcpConnection

    connection = new ReactorNetty2TcpConnection<>(inbound, outbound, codec, completionSink); - scheduler.schedule(() -> this.connectionHandler.afterConnected(connection)); - - inbound.withConnection(conn -> conn.addHandlerFirst(new StompMessageDecoder<>(codec))); - - inbound.receiveObject() - .cast(Message.class) - .publishOn(scheduler, PUBLISH_ON_BUFFER_SIZE) - .subscribe( - this.connectionHandler::handleMessage, - this.connectionHandler::handleFailure, - this.connectionHandler::afterConnectionClosed); - - return completionSink.asMono(); - } - } - - - private static class StompMessageDecoder

    extends ByteToMessageDecoder { - - private final TcpMessageCodec

    codec; - - StompMessageDecoder(TcpMessageCodec

    codec) { - this.codec = codec; - } - - @Override - protected void decode(ChannelHandlerContext ctx, Buffer buffer) throws Exception { - ByteBuffer byteBuffer = ByteBuffer.allocate(buffer.readableBytes()); - buffer.readBytes(byteBuffer); - byteBuffer.flip(); - List> messages = this.codec.decode(byteBuffer); - for (Message

    message : messages) { - ctx.fireChannelRead(message); - } - } - - } - -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNetty2TcpConnection.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNetty2TcpConnection.java deleted file mode 100644 index 12da763ab6d8..000000000000 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNetty2TcpConnection.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.tcp.reactor; - -import java.nio.ByteBuffer; -import java.util.concurrent.CompletableFuture; - -import io.netty5.buffer.Buffer; -import reactor.core.publisher.Mono; -import reactor.core.publisher.Sinks; -import reactor.netty5.NettyInbound; -import reactor.netty5.NettyOutbound; - -import org.springframework.messaging.Message; -import org.springframework.messaging.tcp.TcpConnection; - -/** - * Reactor Netty based implementation of {@link TcpConnection}. - * - *

    This class is based on {@link ReactorNettyTcpConnection}. - * - * @author Rossen Stoyanchev - * @since 6.0 - * @param

    the type of payload for outbound messages - */ -public class ReactorNetty2TcpConnection

    implements TcpConnection

    { - - private final NettyInbound inbound; - - private final NettyOutbound outbound; - - private final TcpMessageCodec

    codec; - - private final Sinks.Empty completionSink; - - - public ReactorNetty2TcpConnection(NettyInbound inbound, NettyOutbound outbound, - TcpMessageCodec

    codec, Sinks.Empty completionSink) { - - this.inbound = inbound; - this.outbound = outbound; - this.codec = codec; - this.completionSink = completionSink; - } - - - @Override - public CompletableFuture sendAsync(Message

    message) { - ByteBuffer byteBuffer = this.codec.encode(message); - Buffer buffer = this.outbound.alloc().copyOf(byteBuffer); - return this.outbound.send(Mono.just(buffer)).then().toFuture(); - } - - @Override - public void onReadInactivity(Runnable runnable, long inactivityDuration) { - this.inbound.withConnection(conn -> conn.onReadIdle(inactivityDuration, runnable)); - } - - @Override - public void onWriteInactivity(Runnable runnable, long inactivityDuration) { - this.inbound.withConnection(conn -> conn.onWriteIdle(inactivityDuration, runnable)); - } - - @Override - public void close() { - // Ignore result: concurrent attempts to complete are ok - this.completionSink.tryEmitEmpty(); - } - - @Override - public String toString() { - return "ReactorNetty2TcpConnection[inbound=" + this.inbound + "]"; - } -} diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java index 35ff79fc7818..106496a3f903 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/ReactorNettyTcpClient.java @@ -31,6 +31,7 @@ import io.netty.util.concurrent.ImmediateEventExecutor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -45,7 +46,6 @@ import reactor.netty.tcp.TcpClient; import reactor.util.retry.Retry; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.tcp.ReconnectStrategy; import org.springframework.messaging.tcp.TcpConnection; @@ -70,14 +70,11 @@ public class ReactorNettyTcpClient

    implements TcpOperations

    { private final ReactorNettyCodec

    codec; - @Nullable - private final ChannelGroup channelGroup; + private final @Nullable ChannelGroup channelGroup; - @Nullable - private final LoopResources loopResources; + private final @Nullable LoopResources loopResources; - @Nullable - private final ConnectionProvider poolResources; + private final @Nullable ConnectionProvider poolResources; private final Scheduler scheduler = Schedulers.newParallel("tcp-client-scheduler"); diff --git a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/package-info.java b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/package-info.java index 0fbf6c5be757..976a3d45c031 100644 --- a/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/package-info.java +++ b/spring-messaging/src/main/java/org/springframework/messaging/tcp/reactor/package-info.java @@ -1,9 +1,7 @@ /** * Contains support for TCP messaging based on Reactor. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.messaging.tcp.reactor; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 0f3cc097dd44..a3a56244178f 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 @@ * @author Rossen Stoyanchev * @author Sebastien Deleuze */ +@SuppressWarnings("removal") class MappingJackson2MessageConverterTests { @Test diff --git a/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java b/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java index 320fd1adc121..adce141d90df 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/converter/MessageConverterTests.java @@ -19,9 +19,9 @@ import java.util.HashMap; import java.util.Map; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.simp.SimpMessageHeaderAccessor; 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 4fb1006d1ddf..cc26993e814e 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 @@ */ 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 @@ void send() { @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 @@ protected Message doSendAndReceive(MessageChannel channel, Message request } } -} -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 98f707b3f048..3d0731280368 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.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 @@ */ 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 @@ void sendToDestination() { @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 @@ void convertAndSendPayloadWithPostProcessorToDestination() { @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 @@ protected void doSend(String destination, Message message) { } } - -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 000000000000..81275d0cf91e --- /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/MessageMappingReflectiveProcessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java index 32a3299bb766..3d109d6f230b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessageMappingReflectiveProcessorTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -54,7 +54,7 @@ void registerReflectiveHintsForMethodWithReturnValue() throws NoSuchMethodExcept assertThat(typeHint.getType()).isEqualTo(TypeReference.of(OutgoingMessage.class)); assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder( MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.DECLARED_FIELDS); + MemberCategory.ACCESS_DECLARED_FIELDS); assertThat(typeHint.methods()).satisfiesExactlyInAnyOrder( hint -> assertThat(hint.getName()).isEqualTo("getMessage"), hint -> assertThat(hint.getName()).isEqualTo("setMessage")); @@ -72,7 +72,7 @@ void registerReflectiveHintsForMethodWithExplicitPayload() throws NoSuchMethodEx assertThat(typeHint.getType()).isEqualTo(TypeReference.of(IncomingMessage.class)); assertThat(typeHint.getMemberCategories()).containsExactlyInAnyOrder( MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, - MemberCategory.DECLARED_FIELDS); + MemberCategory.ACCESS_DECLARED_FIELDS); assertThat(typeHint.methods()).satisfiesExactlyInAnyOrder( hint -> assertThat(hint.getName()).isEqualTo("getMessage"), hint -> assertThat(hint.getName()).isEqualTo("setMessage")); @@ -123,14 +123,14 @@ void registerReflectiveHintsForClass() { void registerReflectiveHintsForMethodWithSubscribeMapping() throws NoSuchMethodException { Method method = SampleController.class.getDeclaredMethod("handleSubscribe"); processor.registerReflectionHints(hints.reflection(), method); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleController.class, "handleSubscribe")).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleController.class, "handleSubscribe")).accepts(hints); } @Test void registerReflectiveHintsForMethodWithMessageExceptionHandler() throws NoSuchMethodException { Method method = SampleController.class.getDeclaredMethod("handleIOException"); processor.registerReflectionHints(hints.reflection(), method); - assertThat(RuntimeHintsPredicates.reflection().onMethod(SampleController.class, "handleIOException")).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(SampleController.class, "handleIOException")).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(IOException.class)).accepts(hints); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java index 1c3c826fb0c4..82874614891a 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/MessagingPredicates.java @@ -18,8 +18,9 @@ import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; /** * Predicates for messaging annotations. @@ -55,8 +56,7 @@ public static HeaderPredicate headerPlain() { public static class DestinationVariablePredicate implements Predicate { - @Nullable - private String value; + private @Nullable String value; public DestinationVariablePredicate value(@Nullable String name) { @@ -79,14 +79,11 @@ public boolean test(MethodParameter parameter) { public static class HeaderPredicate implements Predicate { - @Nullable - private String name; + private @Nullable String name; - @Nullable - private Boolean required; + private @Nullable Boolean required; - @Nullable - private String defaultValue; + private @Nullable String defaultValue; public HeaderPredicate name(@Nullable String name) { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolverTests.java index da36141e3064..b63e3bf9dd50 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/annotation/reactive/PayloadMethodArgumentResolverTests.java @@ -23,6 +23,7 @@ import java.util.Collections; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -35,7 +36,6 @@ import org.springframework.core.codec.StringDecoder; import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageHeaders; import org.springframework.messaging.handler.annotation.Payload; @@ -160,8 +160,7 @@ private DataBuffer toDataBuffer(String value) { @SuppressWarnings("unchecked") - @Nullable - private T resolveValue(MethodParameter param, Publisher content, Validator validator) { + private @Nullable T resolveValue(MethodParameter param, Publisher content, Validator validator) { Message message = new GenericMessage<>(content, Collections.singletonMap(MessageHeaders.CONTENT_TYPE, MimeTypeUtils.TEXT_PLAIN)); 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 9f5e884f5758..a214e519eb75 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 @@ void resolveWithConversionEmptyPayloadButNoConverter() { } @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/handler/invocation/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java index cdd1fcb27ad8..06b9fa532225 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/InvocableHandlerMethodTests.java @@ -18,10 +18,10 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import static org.assertj.core.api.Assertions.assertThat; @@ -154,8 +154,7 @@ public void invocationErrorMessage() { .withMessageContaining("Illegal argument"); } - @Nullable - private Object invoke(Object handler, Method method, Object... providedArgs) throws Exception { + private @Nullable Object invoke(Object handler, Method method, Object... providedArgs) throws Exception { InvocableHandlerMethod handlerMethod = new InvocableHandlerMethod(handler, method); handlerMethod.setMessageMethodArgumentResolvers(this.resolvers); return handlerMethod.invoke(this.message, providedArgs); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java index 5fb96aaa9c47..45013cc4f572 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/MethodMessageHandlerTests.java @@ -25,6 +25,7 @@ import java.util.Map; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -236,7 +237,7 @@ protected String getDestination(Message message) { } @Override - protected String getMatchingMapping(String mapping, Message message) { + protected @Nullable String getMatchingMapping(String mapping, Message message) { String destination = getLookupDestination(getDestination(message)); Assert.notNull(destination, "No destination"); return mapping.equals(destination) || this.pathMatcher.match(mapping, destination) ? mapping : null; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java index 31df4ee8cc66..47fc6b28a7ea 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/ResolvableMethod.java @@ -32,6 +32,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.cglib.core.SpringNamingPolicy; import org.springframework.cglib.proxy.Callback; @@ -47,7 +48,6 @@ import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.SynthesizingMethodParameter; -import org.springframework.lang.Nullable; import org.springframework.objenesis.ObjenesisException; import org.springframework.objenesis.SpringObjenesis; import org.springframework.util.Assert; @@ -613,12 +613,10 @@ private List applyFilters() { private static class MethodInvocationInterceptor implements MethodInterceptor, InvocationHandler { - @Nullable - private Method invokedMethod; + private @Nullable Method invokedMethod; @Override - @Nullable - public Object intercept(Object object, Method method, @Nullable Object[] args, @Nullable MethodProxy proxy) { + public @Nullable Object intercept(Object object, Method method, Object @Nullable [] args, @Nullable MethodProxy proxy) { if (ReflectionUtils.isObjectMethod(method)) { return ReflectionUtils.invokeMethod(method, object, args); } @@ -629,13 +627,11 @@ public Object intercept(Object object, Method method, @Nullable Object[] args, @ } @Override - @Nullable - public Object invoke(Object proxy, Method method, @Nullable Object[] args) { + public @Nullable Object invoke(Object proxy, Method method, Object @Nullable [] args) { return intercept(proxy, method, args, null); } - @Nullable - Method getInvokedMethod() { + @Nullable Method getInvokedMethod() { return this.invokedMethod; } } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/StubArgumentResolver.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/StubArgumentResolver.java index fb8e7db1bf26..a2010c444673 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/StubArgumentResolver.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/StubArgumentResolver.java @@ -19,8 +19,9 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -32,8 +33,7 @@ public class StubArgumentResolver implements HandlerMethodArgumentResolver { private final Class valueType; - @Nullable - private final Object value; + private final @Nullable Object value; private List resolvedParameters = new ArrayList<>(); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/EncoderMethodReturnValueHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/EncoderMethodReturnValueHandlerTests.java index d07b83aed619..21c0fab5e55c 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/EncoderMethodReturnValueHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/EncoderMethodReturnValueHandlerTests.java @@ -19,6 +19,7 @@ import java.util.Collections; import io.reactivex.rxjava3.core.Completable; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -27,7 +28,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ReactiveAdapterRegistry; import org.springframework.core.codec.CharSequenceEncoder; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.GenericMessage; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java index 67eb668c3be2..1413e76e3d1c 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/InvocableHandlerMethodTests.java @@ -22,12 +22,12 @@ import java.util.List; import java.util.concurrent.atomic.AtomicReference; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.invocation.MethodArgumentResolutionException; import org.springframework.messaging.handler.invocation.ResolvableMethod; @@ -161,8 +161,7 @@ void voidMonoMethod() { } - @Nullable - private Object invokeAndBlock(Object handler, Method method, Object... providedArgs) { + private @Nullable Object invokeAndBlock(Object handler, Method method, Object... providedArgs) { return invoke(handler, method, providedArgs).block(Duration.ofSeconds(5)); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java index f48f5ce592ad..6078e83ff406 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/MethodMessageHandlerTests.java @@ -25,13 +25,13 @@ import java.util.Set; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import reactor.test.StepVerifier; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.handler.DestinationPatternsMessageCondition; import org.springframework.messaging.handler.HandlerMethod; @@ -218,8 +218,7 @@ protected List initReturnValueHandler return Collections.singletonList(this.returnValueHandler); } - @Nullable - public Object getLastReturnValue() { + public @Nullable Object getLastReturnValue() { return this.returnValueHandler.getLastReturnValue(); } @@ -238,8 +237,7 @@ protected Set getDirectLookupMappings(String mapping) { } @Override - @Nullable - protected RouteMatcher.Route getDestination(Message message) { + protected RouteMatcher.@Nullable Route getDestination(Message message) { return (RouteMatcher.Route) message.getHeaders().get( DestinationPatternsMessageCondition.LOOKUP_DESTINATION_HEADER); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/StubArgumentResolver.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/StubArgumentResolver.java index c5b5b9ecf0b5..12fe1000f26b 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/StubArgumentResolver.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/StubArgumentResolver.java @@ -19,10 +19,10 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -34,8 +34,7 @@ public class StubArgumentResolver implements HandlerMethodArgumentResolver { private final Class valueType; - @Nullable - private final Object value; + private final @Nullable Object value; private List resolvedParameters = new ArrayList<>(); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/TestReturnValueHandler.java b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/TestReturnValueHandler.java index 22ea181370fd..ba83bc7f44ee 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/TestReturnValueHandler.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/handler/invocation/reactive/TestReturnValueHandler.java @@ -16,11 +16,11 @@ package org.springframework.messaging.handler.invocation.reactive; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; /** @@ -29,12 +29,10 @@ */ public class TestReturnValueHandler implements HandlerMethodReturnValueHandler { - @Nullable - private Object lastReturnValue; + private @Nullable Object lastReturnValue; - @Nullable - public Object getLastReturnValue() { + public @Nullable Object getLastReturnValue() { return this.lastReturnValue; } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultMetadataExtractorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultMetadataExtractorTests.java index 9de9f9627aca..baf908d569ca 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultMetadataExtractorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/DefaultMetadataExtractorTests.java @@ -24,6 +24,7 @@ import io.netty.buffer.PooledByteBufAllocator; import io.rsocket.Payload; import io.rsocket.metadata.WellKnownMimeType; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -35,7 +36,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DataBufferUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/TestRSocket.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/TestRSocket.java index 15580a3026b6..17332a4084e1 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/TestRSocket.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/TestRSocket.java @@ -18,12 +18,11 @@ import io.rsocket.Payload; import io.rsocket.RSocket; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; - /** * {@link RSocket} that saves the name of the invoked method and the input payload(s). */ @@ -33,11 +32,11 @@ public class TestRSocket implements RSocket { private Flux payloadFluxToReturn = Flux.empty(); - @Nullable private volatile String savedMethodName; + private volatile @Nullable String savedMethodName; - @Nullable private volatile Payload savedPayload; + private volatile @Nullable Payload savedPayload; - @Nullable private volatile Flux savedPayloadFlux; + private volatile @Nullable Flux savedPayloadFlux; public void setPayloadMonoToReturn(Mono payloadMonoToReturn) { @@ -48,18 +47,15 @@ public void setPayloadFluxToReturn(Flux payloadFluxToReturn) { this.payloadFluxToReturn = payloadFluxToReturn; } - @Nullable - public String getSavedMethodName() { + public @Nullable String getSavedMethodName() { return this.savedMethodName; } - @Nullable - public Payload getSavedPayload() { + public @Nullable Payload getSavedPayload() { return this.savedPayload; } - @Nullable - public Flux getSavedPayloadFlux() { + public @Nullable Flux getSavedPayloadFlux() { return this.savedPayloadFlux; } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageConditionTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageConditionTests.java index 209c2fadfc98..f23fd0ae3526 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageConditionTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/annotation/support/RSocketFrameTypeMessageConditionTests.java @@ -19,9 +19,9 @@ import java.util.Arrays; import io.rsocket.frame.FrameType; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.support.MessageBuilder; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolverTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolverTests.java index f1f5ac9527d1..037bf9355224 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolverTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/PayloadArgumentResolverTests.java @@ -18,6 +18,7 @@ import io.reactivex.rxjava3.core.Completable; import io.reactivex.rxjava3.core.Single; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; @@ -25,7 +26,6 @@ import org.springframework.core.MethodParameter; import org.springframework.core.ParameterizedTypeReference; import org.springframework.core.ReactiveAdapterRegistry; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.Payload; import static org.assertj.core.api.Assertions.assertThat; diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java index ef1fe46c77c6..107e16b25138 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeBeanRegistrationAotProcessorTests.java @@ -16,6 +16,7 @@ package org.springframework.messaging.rsocket.service; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.aop.SpringProxy; @@ -28,7 +29,6 @@ import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.core.DecoratingProxy; -import org.springframework.lang.Nullable; import org.springframework.messaging.handler.annotation.Payload; import static org.assertj.core.api.Assertions.assertThat; @@ -70,8 +70,7 @@ void process(Class beanClass) { } } - @Nullable - private static BeanRegistrationAotContribution createContribution(Class beanClass) { + private static @Nullable BeanRegistrationAotContribution createContribution(Class beanClass) { DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); beanFactory.registerBeanDefinition(beanClass.getName(), new RootBeanDefinition(beanClass)); return new RSocketExchangeBeanRegistrationAotProcessor() diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java index 4c164202dd80..8994e20c3bcb 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketExchangeReflectiveProcessorTests.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,19 +50,19 @@ void shouldRegisterReflectionHintsForMethod() throws NoSuchMethodException { processor.registerReflectionHints(hints.reflection(), method); assertThat(reflection().onType(SampleService.class)).accepts(hints); - assertThat(reflection().onMethod(SampleService.class, "get")).accepts(hints); + assertThat(reflection().onMethodInvocation(SampleService.class, "get")).accepts(hints); assertThat(reflection().onType(Response.class)).accepts(hints); - assertThat(reflection().onMethod(Response.class, "getMessage")).accepts(hints); - assertThat(reflection().onMethod(Response.class, "setMessage")).accepts(hints); + assertThat(reflection().onMethodInvocation(Response.class, "getMessage")).accepts(hints); + assertThat(reflection().onMethodInvocation(Response.class, "setMessage")).accepts(hints); assertThat(reflection().onType(Request.class)).accepts(hints); - assertThat(reflection().onMethod(Request.class, "getMessage")).accepts(hints); - assertThat(reflection().onMethod(Request.class, "setMessage")).accepts(hints); + assertThat(reflection().onMethodInvocation(Request.class, "getMessage")).accepts(hints); + assertThat(reflection().onMethodInvocation(Request.class, "setMessage")).accepts(hints); assertThat(reflection().onType(Variable.class)).accepts(hints); - assertThat(reflection().onMethod(Variable.class, "getValue")).accepts(hints); - assertThat(reflection().onMethod(Variable.class, "setValue")).accepts(hints); + assertThat(reflection().onMethodInvocation(Variable.class, "getValue")).accepts(hints); + assertThat(reflection().onMethodInvocation(Variable.class, "setValue")).accepts(hints); assertThat(reflection().onType(Metadata.class)).accepts(hints); - assertThat(reflection().onMethod(Metadata.class, "getValue")).accepts(hints); - assertThat(reflection().onMethod(Metadata.class, "setValue")).accepts(hints); + assertThat(reflection().onMethodInvocation(Metadata.class, "getValue")).accepts(hints); + assertThat(reflection().onMethodInvocation(Metadata.class, "setValue")).accepts(hints); } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolverTestSupport.java b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolverTestSupport.java index 7b534dcca8a2..6b7b5b5c3e64 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolverTestSupport.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/rsocket/service/RSocketServiceArgumentResolverTestSupport.java @@ -18,8 +18,9 @@ import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodParameter; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** @@ -28,13 +29,11 @@ */ public abstract class RSocketServiceArgumentResolverTestSupport { - @Nullable - private RSocketServiceArgumentResolver resolver; + private @Nullable RSocketServiceArgumentResolver resolver; private final RSocketRequestValues.Builder requestValuesBuilder = RSocketRequestValues.builder(null); - @Nullable - private RSocketRequestValues requestValues; + private @Nullable RSocketRequestValues requestValues; protected RSocketServiceArgumentResolverTestSupport() { 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 ae55bc99d4d5..b12e47224120 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.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 @@ void setup() { 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/SimpAnnotationMethodMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java index 532838457aa5..6369215aa854 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/annotation/support/SimpAnnotationMethodMessageHandlerTests.java @@ -25,6 +25,7 @@ import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -37,7 +38,6 @@ import reactor.core.publisher.Sinks; import org.springframework.context.support.StaticApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHeaders; @@ -273,39 +273,6 @@ void dotPathSeparator() { assertThat(controller.method).isEqualTo("handleFoo"); } - @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) - public void listenableFutureSuccess() { - Message emptyMessage = MessageBuilder.withPayload(new byte[0]).build(); - given(this.channel.send(any(Message.class))).willReturn(true); - given(this.converter.toMessage(any(), any(MessageHeaders.class))).willReturn(emptyMessage); - - ListenableFutureController controller = new ListenableFutureController(); - this.messageHandler.registerHandler(controller); - this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/")); - - Message message = createMessage("/app1/listenable-future/success"); - this.messageHandler.handleMessage(message); - - assertThat(controller.future).isNotNull(); - controller.future.run(); - verify(this.converter).toMessage(this.payloadCaptor.capture(), any(MessageHeaders.class)); - assertThat(this.payloadCaptor.getValue()).isEqualTo("foo"); - } - - @Test - void listenableFutureFailure() { - ListenableFutureController controller = new ListenableFutureController(); - this.messageHandler.registerHandler(controller); - this.messageHandler.setDestinationPrefixes(Arrays.asList("/app1", "/app2/")); - - Message message = createMessage("/app1/listenable-future/failure"); - this.messageHandler.handleMessage(message); - - controller.future.run(); - assertThat(controller.exceptionCaught).isTrue(); - } - @Test @SuppressWarnings({ "unchecked", "rawtypes" }) public void completableFutureSuccess() { @@ -569,36 +536,6 @@ public void handleFoo() { } - @Controller - @MessageMapping("listenable-future") - @SuppressWarnings({"deprecation", "removal"}) - private static class ListenableFutureController { - - org.springframework.util.concurrent.ListenableFutureTask future; - - boolean exceptionCaught = false; - - @MessageMapping("success") - public org.springframework.util.concurrent.ListenableFutureTask handleListenableFuture() { - this.future = new org.springframework.util.concurrent.ListenableFutureTask<>(() -> "foo"); - return this.future; - } - - @MessageMapping("failure") - public org.springframework.util.concurrent.ListenableFutureTask handleListenableFutureException() { - this.future = new org.springframework.util.concurrent.ListenableFutureTask<>(() -> { - throw new IllegalStateException(); - }); - return this.future; - } - - @MessageExceptionHandler(IllegalStateException.class) - public void handleValidationException() { - this.exceptionCaught = true; - } - } - - @Controller private static class CompletableFutureController { 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 0c412d4c615c..d3a144b69204 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 @@ -33,7 +33,7 @@ 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; @@ -92,7 +92,7 @@ void setup() throws Exception { 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"); 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 3445bb0a32c7..6ba2e99affe1 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 @@ -24,6 +24,7 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.Executor; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.context.ApplicationContext; @@ -32,7 +33,6 @@ import org.springframework.context.annotation.Configuration; import org.springframework.context.support.StaticApplicationContext; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.MessageChannel; import org.springframework.messaging.MessageHandler; @@ -40,8 +40,8 @@ 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 @@ void configureMessageConvertersDefault() { 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 @@ protected boolean configureMessageConverters(List messageConve 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/stomp/AbstractStompBrokerRelayIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/AbstractStompBrokerRelayIntegrationTests.java index ed1fe62fa92d..0314c08d8c0d 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/AbstractStompBrokerRelayIntegrationTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/AbstractStompBrokerRelayIntegrationTests.java @@ -538,8 +538,8 @@ protected boolean matchInternal(StompHeaderAccessor headers, Object payload) { @Override public String toString() { - return super.toString() + ", subscriptionId=\"" + this.subscriptionId - + "\", destination=\"" + this.destination + "\", payload=\"" + getPayloadAsText() + "\""; + return super.toString() + ", subscriptionId=\"" + this.subscriptionId + + "\", destination=\"" + this.destination + "\", payload=\"" + getPayloadAsText() + "\""; } protected String getPayloadAsText() { diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNetty2StompBrokerRelayIntegrationTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNetty2StompBrokerRelayIntegrationTests.java deleted file mode 100644 index f3b502f53f6c..000000000000 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNetty2StompBrokerRelayIntegrationTests.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.simp.stomp; - -import org.junit.jupiter.api.Disabled; - -import org.springframework.messaging.tcp.TcpOperations; -import org.springframework.messaging.tcp.reactor.ReactorNetty2TcpClient; - -/** - * Integration tests for {@link StompBrokerRelayMessageHandler} running against - * ActiveMQ with {@link ReactorNetty2TcpClient}. - * - * @author Rossen Stoyanchev - */ -@Disabled("gh-29287 :: Disabled because they fail too frequently") -public class ReactorNetty2StompBrokerRelayIntegrationTests extends AbstractStompBrokerRelayIntegrationTests { - - @Override - protected TcpOperations initTcpClient(int port) { - return new ReactorNetty2TcpClient<>("127.0.0.1", port, new StompTcpMessageCodec()); - } - -} diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java index 65d54e36164c..be9bd4d67fad 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/ReactorNettyTcpStompClientTests.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,15 +29,14 @@ import org.apache.activemq.broker.TransportConnector; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.TestInfo; -import org.springframework.lang.Nullable; import org.springframework.messaging.converter.StringMessageConverter; import org.springframework.messaging.simp.stomp.StompSession.Subscription; -import org.springframework.messaging.tcp.reactor.ReactorNetty2TcpClient; import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler; import org.springframework.util.Assert; @@ -58,8 +57,6 @@ class ReactorNettyTcpStompClientTests { private ReactorNettyTcpStompClient client; - private ReactorNettyTcpStompClient client2; - @BeforeEach void setup(TestInfo testInfo) throws Exception { @@ -84,10 +81,6 @@ void setup(TestInfo testInfo) throws Exception { this.client = new ReactorNettyTcpStompClient(host, port); this.client.setMessageConverter(new StringMessageConverter()); this.client.setTaskScheduler(taskScheduler); - - this.client2 = new ReactorNettyTcpStompClient(new ReactorNetty2TcpClient<>(host, port, new StompTcpMessageCodec())); - this.client2.setMessageConverter(new StringMessageConverter()); - this.client2.setTaskScheduler(taskScheduler); } private TransportConnector createStompConnector() throws Exception { @@ -100,7 +93,6 @@ private TransportConnector createStompConnector() throws Exception { void shutdown() throws Exception { try { this.client.shutdown(); - this.client2.shutdown(); } catch (Throwable ex) { logger.error("Failed to shut client", ex); @@ -120,11 +112,6 @@ void publishSubscribeOnReactorNetty() throws Exception { testPublishSubscribe(this.client); } - @Test - void publishSubscribeOnReactorNetty2() throws Exception { - testPublishSubscribe(this.client2); - } - private void testPublishSubscribe(ReactorNettyTcpStompClient clientToUse) throws Exception { String destination = "/topic/foo"; ConsumingHandler consumingHandler1 = new ConsumingHandler(destination); diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java index a5d264067132..e127c17685c3 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/stomp/SplittingStompEncoderTests.java @@ -20,10 +20,9 @@ import java.nio.charset.StandardCharsets; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; - import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; 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 af866b37e8a0..01f7e0ae4e52 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 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/TestSimpSession.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSession.java index 133116e9898b..a0926df29494 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSession.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSession.java @@ -19,7 +19,7 @@ import java.util.HashSet; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Rossen Stoyanchev diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSubscription.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSubscription.java index bad87cf42c44..5dc420936b25 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSubscription.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpSubscription.java @@ -18,7 +18,8 @@ import java.util.Objects; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.ObjectUtils; /** diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpUser.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpUser.java index 548d820646bb..736568ac9d26 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpUser.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/TestSimpUser.java @@ -22,7 +22,7 @@ import java.util.Map; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * @author Rossen Stoyanchev @@ -44,9 +44,8 @@ public String getName() { return name; } - @Nullable @Override - public Principal getPrincipal() { + public @Nullable Principal getPrincipal() { return null; } diff --git a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java index cfcfc1d08d71..e1900f098811 100644 --- a/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java +++ b/spring-messaging/src/test/java/org/springframework/messaging/simp/user/UserDestinationMessageHandlerTests.java @@ -19,12 +19,12 @@ import java.nio.charset.StandardCharsets; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mockito; import org.springframework.core.testfixture.security.TestPrincipal; -import org.springframework.lang.Nullable; import org.springframework.messaging.Message; import org.springframework.messaging.StubMessageChannel; import org.springframework.messaging.SubscribableChannel; 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 4c9c4f6717e9..a21562df3f07 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.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 42aae2a3715f..cc4078c35bca 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 @@ /** * @author Mark Fisher * @author Rossen Stoyanchev + * @author Mengqi Xu */ class MessageBuilderTests { @@ -238,4 +239,22 @@ void buildMultipleMessages() { 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/spring-orm.gradle b/spring-orm/spring-orm.gradle index db00899a11f4..e9cd86a53dd9 100644 --- a/spring-orm/spring-orm.gradle +++ b/spring-orm/spring-orm.gradle @@ -11,7 +11,7 @@ dependencies { optional(project(":spring-web")) optional("jakarta.servlet:jakarta.servlet-api") optional("org.eclipse.persistence:org.eclipse.persistence.jpa") - optional("org.hibernate:hibernate-core-jakarta") + optional("org.hibernate.orm:hibernate-core") testImplementation(project(":spring-core-test")) testImplementation(testFixtures(project(":spring-beans"))) testImplementation(testFixtures(project(":spring-context"))) diff --git a/spring-orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java b/spring-orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java index 0799a5033ee1..8fc038ed6bf3 100644 --- a/spring-orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java +++ b/spring-orm/src/main/java/org/springframework/orm/ObjectOptimisticLockingFailureException.java @@ -16,8 +16,9 @@ package org.springframework.orm; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.OptimisticLockingFailureException; -import org.springframework.lang.Nullable; /** * Exception thrown on an optimistic locking violation for a mapped object. @@ -29,11 +30,9 @@ @SuppressWarnings("serial") public class ObjectOptimisticLockingFailureException extends OptimisticLockingFailureException { - @Nullable - private final Object persistentClass; + private final @Nullable Object persistentClass; - @Nullable - private final Object identifier; + private final @Nullable Object identifier; /** @@ -135,8 +134,7 @@ public ObjectOptimisticLockingFailureException( * Return the persistent class of the object for which the locking failed. * If no Class was specified, this method returns null. */ - @Nullable - public Class getPersistentClass() { + public @Nullable Class getPersistentClass() { return (this.persistentClass instanceof Class clazz ? clazz : null); } @@ -144,8 +142,7 @@ public Class getPersistentClass() { * Return the name of the persistent class of the object for which the locking failed. * Will work for both Class objects and String names. */ - @Nullable - public String getPersistentClassName() { + public @Nullable String getPersistentClassName() { if (this.persistentClass instanceof Class clazz) { return clazz.getName(); } @@ -155,8 +152,7 @@ public String getPersistentClassName() { /** * Return the identifier of the object for which the locking failed. */ - @Nullable - public Object getIdentifier() { + public @Nullable Object getIdentifier() { return this.identifier; } diff --git a/spring-orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java b/spring-orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java index 54536f5d5705..7a359a7ca61c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java +++ b/spring-orm/src/main/java/org/springframework/orm/ObjectRetrievalFailureException.java @@ -16,8 +16,9 @@ package org.springframework.orm; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataRetrievalFailureException; -import org.springframework.lang.Nullable; /** * Exception thrown if a mapped object could not be retrieved via its identifier. @@ -29,11 +30,9 @@ @SuppressWarnings("serial") public class ObjectRetrievalFailureException extends DataRetrievalFailureException { - @Nullable - private final Object persistentClass; + private final @Nullable Object persistentClass; - @Nullable - private final Object identifier; + private final @Nullable Object identifier; /** @@ -109,8 +108,7 @@ public ObjectRetrievalFailureException( * Return the persistent class of the object that was not found. * If no Class was specified, this method returns null. */ - @Nullable - public Class getPersistentClass() { + public @Nullable Class getPersistentClass() { return (this.persistentClass instanceof Class clazz ? clazz : null); } @@ -118,8 +116,7 @@ public Class getPersistentClass() { * Return the name of the persistent class of the object that was not found. * Will work for both Class objects and String names. */ - @Nullable - public String getPersistentClassName() { + public @Nullable String getPersistentClassName() { if (this.persistentClass instanceof Class clazz) { return clazz.getName(); } @@ -129,8 +126,7 @@ public String getPersistentClassName() { /** * Return the identifier of the object that was not found. */ - @Nullable - public Object getIdentifier() { + public @Nullable Object getIdentifier() { return this.identifier; } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java index 38265a6f1f9d..084adf9fedcf 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/ConfigurableJtaPlatform.java @@ -25,8 +25,8 @@ import jakarta.transaction.UserTransaction; import org.hibernate.TransactionException; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.transaction.jta.UserTransactionAdapter; import org.springframework.util.Assert; @@ -44,8 +44,7 @@ class ConfigurableJtaPlatform implements JtaPlatform { private final UserTransaction userTransaction; - @Nullable - private final TransactionSynchronizationRegistry transactionSynchronizationRegistry; + private final @Nullable TransactionSynchronizationRegistry transactionSynchronizationRegistry; /** diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java deleted file mode 100644 index a5515947bc8c..000000000000 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateCallback.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright 2002-2018 the original author 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.orm.hibernate5; - -import org.hibernate.HibernateException; -import org.hibernate.Session; - -import org.springframework.lang.Nullable; - -/** - * Callback interface for Hibernate code. To be used with {@link HibernateTemplate}'s - * execution methods, often as anonymous classes within a method implementation. - * A typical implementation will call {@code Session.load/find/update} to perform - * some operations on persistent objects. - * - * @author Juergen Hoeller - * @since 4.2 - * @param the result type - * @see HibernateTemplate - * @see HibernateTransactionManager - */ -@FunctionalInterface -public interface HibernateCallback { - - /** - * Gets called by {@code HibernateTemplate.execute} with an active - * Hibernate {@code Session}. Does not need to care about activating - * or closing the {@code Session}, or handling transactions. - *

    Allows for returning a result object created within the callback, - * i.e. a domain object or a collection of domain objects. - * A thrown custom RuntimeException is treated as an application exception: - * It gets propagated to the caller of the template. - * @param session active Hibernate session - * @return a result object, or {@code null} if none - * @throws HibernateException if thrown by the Hibernate API - * @see HibernateTemplate#execute - */ - @Nullable - T doInHibernate(Session session) throws HibernateException; - -} diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java index fc247d49f5b7..dcc9978498de 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateExceptionTranslator.java @@ -19,11 +19,11 @@ import jakarta.persistence.PersistenceException; import org.hibernate.HibernateException; import org.hibernate.JDBCException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.jdbc.support.SQLExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerFactoryUtils; /** @@ -45,8 +45,7 @@ */ public class HibernateExceptionTranslator implements PersistenceExceptionTranslator { - @Nullable - private SQLExceptionTranslator jdbcExceptionTranslator; + private @Nullable SQLExceptionTranslator jdbcExceptionTranslator; /** @@ -66,8 +65,7 @@ public void setJdbcExceptionTranslator(SQLExceptionTranslator jdbcExceptionTrans @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { if (ex instanceof HibernateException hibernateEx) { return convertHibernateAccessException(hibernateEx); } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java index 3e3674502cbb..0f0a8315ac62 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateJdbcException.java @@ -19,9 +19,9 @@ import java.sql.SQLException; import org.hibernate.JDBCException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; /** * Hibernate-specific subclass of UncategorizedDataAccessException, @@ -42,7 +42,7 @@ public HibernateJdbcException(JDBCException ex) { /** * Return the underlying SQLException. */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // JDBCException instances always have a non null cause public SQLException getSQLException() { return ((JDBCException) getCause()).getSQLException(); } @@ -50,9 +50,8 @@ public SQLException getSQLException() { /** * Return the SQL that led to the problem. */ - @Nullable - @SuppressWarnings("NullAway") - public String getSql() { + @SuppressWarnings("NullAway") // JDBCException instances always have a non null cause + public @Nullable String getSql() { return ((JDBCException) getCause()).getSQL(); } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java index 150ae55c6d1d..e06342c26a6c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateObjectRetrievalFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,13 +16,10 @@ package org.springframework.orm.hibernate5; -import org.hibernate.HibernateException; import org.hibernate.UnresolvableObjectException; import org.hibernate.WrongClassException; -import org.springframework.lang.Nullable; import org.springframework.orm.ObjectRetrievalFailureException; -import org.springframework.util.ReflectionUtils; /** * Hibernate-specific subclass of ObjectRetrievalFailureException. @@ -36,24 +33,11 @@ public class HibernateObjectRetrievalFailureException extends ObjectRetrievalFailureException { public HibernateObjectRetrievalFailureException(UnresolvableObjectException ex) { - super(ex.getEntityName(), getIdentifier(ex), ex.getMessage(), ex); + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); } public HibernateObjectRetrievalFailureException(WrongClassException ex) { - super(ex.getEntityName(), getIdentifier(ex), ex.getMessage(), ex); - } - - - @Nullable - static Object getIdentifier(HibernateException hibEx) { - try { - // getIdentifier declares Serializable return value on 5.x but Object on 6.x - // -> not binary compatible, let's invoke it reflectively for the time being - return ReflectionUtils.invokeMethod(hibEx.getClass().getMethod("getIdentifier"), hibEx); - } - catch (NoSuchMethodException ex) { - return null; - } + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOperations.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOperations.java deleted file mode 100644 index 0bd481af29f9..000000000000 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOperations.java +++ /dev/null @@ -1,857 +0,0 @@ -/* - * Copyright 2002-2021 the original author 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.orm.hibernate5; - -import java.io.Serializable; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import org.hibernate.Filter; -import org.hibernate.LockMode; -import org.hibernate.ReplicationMode; -import org.hibernate.criterion.DetachedCriteria; - -import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; - -/** - * Interface that specifies a common set of Hibernate operations as well as - * a general {@link #execute} method for Session-based lambda expressions. - * Implemented by {@link HibernateTemplate}. Not often used, but a useful option - * to enhance testability, as it can easily be mocked or stubbed. - * - *

    Defines {@code HibernateTemplate}'s data access methods that mirror various - * {@link org.hibernate.Session} methods. Users are strongly encouraged to read the - * Hibernate {@code Session} javadocs for details on the semantics of those methods. - * - *

    A deprecation note: While {@link HibernateTemplate} and this operations - * interface are being kept around for backwards compatibility in terms of the data - * access implementation style in Spring applications, we strongly recommend the use - * of native {@link org.hibernate.Session} access code for non-trivial interactions. - * This in particular affects parameterized queries where - on Java 8+ - a custom - * {@link HibernateCallback} lambda code block with {@code createQuery} and several - * {@code setParameter} calls on the {@link org.hibernate.query.Query} interface - * is an elegant solution, to be executed via the general {@link #execute} method. - * All such operations which benefit from a lambda variant have been marked as - * {@code deprecated} on this interface. - * - *

    A Hibernate compatibility note: {@link HibernateTemplate} and the - * operations on this interface generally aim to be applicable across all Hibernate - * versions. In terms of binary compatibility, Spring ships a variant for each major - * generation of Hibernate (in the present case: Hibernate ORM 5.x). However, due to - * refactorings and removals in Hibernate ORM 5.3, some variants - in particular - * legacy positional parameters starting from index 0 - do not work anymore. - * All affected operations are marked as deprecated; please replace them with the - * general {@link #execute} method and custom lambda blocks creating the queries, - * ideally setting named parameters through {@link org.hibernate.query.Query}. - * Please be aware that deprecated operations are known to work with Hibernate - * ORM 5.2 but may not work with Hibernate ORM 5.3 and higher anymore. - * - * @author Juergen Hoeller - * @since 4.2 - * @see HibernateTemplate - * @see org.hibernate.Session - * @see HibernateTransactionManager - */ -public interface HibernateOperations { - - /** - * Execute the action specified by the given action object within a - * {@link org.hibernate.Session}. - *

    Application exceptions thrown by the action object get propagated - * to the caller (can only be unchecked). Hibernate exceptions are - * transformed into appropriate DAO ones. Allows for returning a result - * object, that is a domain object or a collection of domain objects. - *

    Note: Callback code is not supposed to handle transactions itself! - * Use an appropriate transaction manager like - * {@link HibernateTransactionManager}. Generally, callback code must not - * touch any {@code Session} lifecycle methods, like close, - * disconnect, or reconnect, to let the template do its work. - * @param action callback object that specifies the Hibernate action - * @return a result object returned by the action, or {@code null} - * @throws DataAccessException in case of Hibernate errors - * @see HibernateTransactionManager - * @see org.hibernate.Session - */ - @Nullable - T execute(HibernateCallback action) throws DataAccessException; - - - //------------------------------------------------------------------------- - // Convenience methods for loading individual objects - //------------------------------------------------------------------------- - - /** - * Return the persistent instance of the given entity class - * with the given identifier, or {@code null} if not found. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#get(Class, Serializable)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityClass a persistent class - * @param id the identifier of the persistent instance - * @return the persistent instance, or {@code null} if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#get(Class, Serializable) - */ - @Nullable - T get(Class entityClass, Serializable id) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, or {@code null} if not found. - *

    Obtains the specified lock mode if the instance exists. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#get(Class, Serializable, LockMode)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityClass a persistent class - * @param id the identifier of the persistent instance - * @param lockMode the lock mode to obtain - * @return the persistent instance, or {@code null} if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#get(Class, Serializable, LockMode) - */ - @Nullable - T get(Class entityClass, Serializable id, LockMode lockMode) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, or {@code null} if not found. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#get(String, Serializable)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityName the name of the persistent entity - * @param id the identifier of the persistent instance - * @return the persistent instance, or {@code null} if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#get(Class, Serializable) - */ - @Nullable - Object get(String entityName, Serializable id) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, or {@code null} if not found. - * Obtains the specified lock mode if the instance exists. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#get(String, Serializable, LockMode)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityName the name of the persistent entity - * @param id the identifier of the persistent instance - * @param lockMode the lock mode to obtain - * @return the persistent instance, or {@code null} if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#get(Class, Serializable, LockMode) - */ - @Nullable - Object get(String entityName, Serializable id, LockMode lockMode) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, throwing an exception if not found. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#load(Class, Serializable)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityClass a persistent class - * @param id the identifier of the persistent instance - * @return the persistent instance - * @throws org.springframework.orm.ObjectRetrievalFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#load(Class, Serializable) - */ - T load(Class entityClass, Serializable id) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, throwing an exception if not found. - * Obtains the specified lock mode if the instance exists. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#load(Class, Serializable, LockMode)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityClass a persistent class - * @param id the identifier of the persistent instance - * @param lockMode the lock mode to obtain - * @return the persistent instance - * @throws org.springframework.orm.ObjectRetrievalFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#load(Class, Serializable) - */ - T load(Class entityClass, Serializable id, LockMode lockMode) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, throwing an exception if not found. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#load(String, Serializable)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityName the name of the persistent entity - * @param id the identifier of the persistent instance - * @return the persistent instance - * @throws org.springframework.orm.ObjectRetrievalFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#load(Class, Serializable) - */ - Object load(String entityName, Serializable id) throws DataAccessException; - - /** - * Return the persistent instance of the given entity class - * with the given identifier, throwing an exception if not found. - *

    Obtains the specified lock mode if the instance exists. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#load(String, Serializable, LockMode)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entityName the name of the persistent entity - * @param id the identifier of the persistent instance - * @param lockMode the lock mode to obtain - * @return the persistent instance - * @throws org.springframework.orm.ObjectRetrievalFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#load(Class, Serializable) - */ - Object load(String entityName, Serializable id, LockMode lockMode) throws DataAccessException; - - /** - * Return all persistent instances of the given entity class. - * Note: Use queries or criteria for retrieving a specific subset. - * @param entityClass a persistent class - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException if there is a Hibernate error - * @see org.hibernate.Session#createCriteria - */ - List loadAll(Class entityClass) throws DataAccessException; - - /** - * Load the persistent instance with the given identifier - * into the given object, throwing an exception if not found. - *

    This method is a thin wrapper around - * {@link org.hibernate.Session#load(Object, Serializable)} for convenience. - * For an explanation of the exact semantics of this method, please do refer to - * the Hibernate API documentation in the first instance. - * @param entity the object (of the target class) to load into - * @param id the identifier of the persistent instance - * @throws org.springframework.orm.ObjectRetrievalFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#load(Object, Serializable) - */ - void load(Object entity, Serializable id) throws DataAccessException; - - /** - * Re-read the state of the given persistent instance. - * @param entity the persistent instance to re-read - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#refresh(Object) - */ - void refresh(Object entity) throws DataAccessException; - - /** - * Re-read the state of the given persistent instance. - * Obtains the specified lock mode for the instance. - * @param entity the persistent instance to re-read - * @param lockMode the lock mode to obtain - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#refresh(Object, LockMode) - */ - void refresh(Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Check whether the given object is in the Session cache. - * @param entity the persistence instance to check - * @return whether the given object is in the Session cache - * @throws DataAccessException if there is a Hibernate error - * @see org.hibernate.Session#contains - */ - boolean contains(Object entity) throws DataAccessException; - - /** - * Remove the given object from the {@link org.hibernate.Session} cache. - * @param entity the persistent instance to evict - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#evict - */ - void evict(Object entity) throws DataAccessException; - - /** - * Force initialization of a Hibernate proxy or persistent collection. - * @param proxy a proxy for a persistent object or a persistent collection - * @throws DataAccessException if we can't initialize the proxy, for example - * because it is not associated with an active Session - * @see org.hibernate.Hibernate#initialize - */ - void initialize(Object proxy) throws DataAccessException; - - /** - * Return an enabled Hibernate {@link Filter} for the given filter name. - * The returned {@code Filter} instance can be used to set filter parameters. - * @param filterName the name of the filter - * @return the enabled Hibernate {@code Filter} (either already - * enabled or enabled on the fly by this operation) - * @throws IllegalStateException if we are not running within a - * transactional Session (in which case this operation does not make sense) - */ - Filter enableFilter(String filterName) throws IllegalStateException; - - - //------------------------------------------------------------------------- - // Convenience methods for storing individual objects - //------------------------------------------------------------------------- - - /** - * Obtain the specified lock level upon the given object, implicitly - * checking whether the corresponding database entry still exists. - * @param entity the persistent instance to lock - * @param lockMode the lock mode to obtain - * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#lock(Object, LockMode) - */ - void lock(Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Obtain the specified lock level upon the given object, implicitly - * checking whether the corresponding database entry still exists. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to lock - * @param lockMode the lock mode to obtain - * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#lock(String, Object, LockMode) - */ - void lock(String entityName, Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Persist the given transient instance. - * @param entity the transient instance to persist - * @return the generated identifier - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#save(Object) - */ - Serializable save(Object entity) throws DataAccessException; - - /** - * Persist the given transient instance. - * @param entityName the name of the persistent entity - * @param entity the transient instance to persist - * @return the generated identifier - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#save(String, Object) - */ - Serializable save(String entityName, Object entity) throws DataAccessException; - - /** - * Update the given persistent instance, - * associating it with the current Hibernate {@link org.hibernate.Session}. - * @param entity the persistent instance to update - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#update(Object) - */ - void update(Object entity) throws DataAccessException; - - /** - * Update the given persistent instance, - * associating it with the current Hibernate {@link org.hibernate.Session}. - *

    Obtains the specified lock mode if the instance exists, implicitly - * checking whether the corresponding database entry still exists. - * @param entity the persistent instance to update - * @param lockMode the lock mode to obtain - * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#update(Object) - */ - void update(Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Update the given persistent instance, - * associating it with the current Hibernate {@link org.hibernate.Session}. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to update - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#update(String, Object) - */ - void update(String entityName, Object entity) throws DataAccessException; - - /** - * Update the given persistent instance, - * associating it with the current Hibernate {@link org.hibernate.Session}. - *

    Obtains the specified lock mode if the instance exists, implicitly - * checking whether the corresponding database entry still exists. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to update - * @param lockMode the lock mode to obtain - * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#update(String, Object) - */ - void update(String entityName, Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Save or update the given persistent instance, - * according to its id (matching the configured "unsaved-value"?). - * Associates the instance with the current Hibernate {@link org.hibernate.Session}. - * @param entity the persistent instance to save or update - * (to be associated with the Hibernate {@code Session}) - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#saveOrUpdate(Object) - */ - void saveOrUpdate(Object entity) throws DataAccessException; - - /** - * Save or update the given persistent instance, - * according to its id (matching the configured "unsaved-value"?). - * Associates the instance with the current Hibernate {@code Session}. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to save or update - * (to be associated with the Hibernate {@code Session}) - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#saveOrUpdate(String, Object) - */ - void saveOrUpdate(String entityName, Object entity) throws DataAccessException; - - /** - * Persist the state of the given detached instance according to the - * given replication mode, reusing the current identifier value. - * @param entity the persistent object to replicate - * @param replicationMode the Hibernate ReplicationMode - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#replicate(Object, ReplicationMode) - */ - void replicate(Object entity, ReplicationMode replicationMode) throws DataAccessException; - - /** - * Persist the state of the given detached instance according to the - * given replication mode, reusing the current identifier value. - * @param entityName the name of the persistent entity - * @param entity the persistent object to replicate - * @param replicationMode the Hibernate ReplicationMode - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#replicate(String, Object, ReplicationMode) - */ - void replicate(String entityName, Object entity, ReplicationMode replicationMode) throws DataAccessException; - - /** - * Persist the given transient instance. Follows JSR-220 semantics. - *

    Similar to {@code save}, associating the given object - * with the current Hibernate {@link org.hibernate.Session}. - * @param entity the persistent instance to persist - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#persist(Object) - * @see #save - */ - void persist(Object entity) throws DataAccessException; - - /** - * Persist the given transient instance. Follows JSR-220 semantics. - *

    Similar to {@code save}, associating the given object - * with the current Hibernate {@link org.hibernate.Session}. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to persist - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#persist(String, Object) - * @see #save - */ - void persist(String entityName, Object entity) throws DataAccessException; - - /** - * Copy the state of the given object onto the persistent object - * with the same identifier. Follows JSR-220 semantics. - *

    Similar to {@code saveOrUpdate}, but never associates the given - * object with the current Hibernate Session. In case of a new entity, - * the state will be copied over as well. - *

    Note that {@code merge} will not update the identifiers - * in the passed-in object graph (in contrast to TopLink)! Consider - * registering Spring's {@code IdTransferringMergeEventListener} if - * you would like to have newly assigned ids transferred to the original - * object graph too. - * @param entity the object to merge with the corresponding persistence instance - * @return the updated, registered persistent instance - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#merge(Object) - * @see #saveOrUpdate - */ - T merge(T entity) throws DataAccessException; - - /** - * Copy the state of the given object onto the persistent object - * with the same identifier. Follows JSR-220 semantics. - *

    Similar to {@code saveOrUpdate}, but never associates the given - * object with the current Hibernate {@link org.hibernate.Session}. In - * the case of a new entity, the state will be copied over as well. - *

    Note that {@code merge} will not update the identifiers - * in the passed-in object graph (in contrast to TopLink)! Consider - * registering Spring's {@code IdTransferringMergeEventListener} - * if you would like to have newly assigned ids transferred to the - * original object graph too. - * @param entityName the name of the persistent entity - * @param entity the object to merge with the corresponding persistence instance - * @return the updated, registered persistent instance - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#merge(String, Object) - * @see #saveOrUpdate - */ - T merge(String entityName, T entity) throws DataAccessException; - - /** - * Delete the given persistent instance. - * @param entity the persistent instance to delete - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#delete(Object) - */ - void delete(Object entity) throws DataAccessException; - - /** - * Delete the given persistent instance. - *

    Obtains the specified lock mode if the instance exists, implicitly - * checking whether the corresponding database entry still exists. - * @param entity the persistent instance to delete - * @param lockMode the lock mode to obtain - * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#delete(Object) - */ - void delete(Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Delete the given persistent instance. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to delete - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#delete(Object) - */ - void delete(String entityName, Object entity) throws DataAccessException; - - /** - * Delete the given persistent instance. - *

    Obtains the specified lock mode if the instance exists, implicitly - * checking whether the corresponding database entry still exists. - * @param entityName the name of the persistent entity - * @param entity the persistent instance to delete - * @param lockMode the lock mode to obtain - * @throws org.springframework.orm.ObjectOptimisticLockingFailureException if not found - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#delete(Object) - */ - void delete(String entityName, Object entity, LockMode lockMode) throws DataAccessException; - - /** - * Delete all given persistent instances. - *

    This can be combined with any of the find methods to delete by query - * in two lines of code. - * @param entities the persistent instances to delete - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#delete(Object) - */ - void deleteAll(Collection entities) throws DataAccessException; - - /** - * Flush all pending saves, updates and deletes to the database. - *

    Only invoke this for selective eager flushing, for example when - * JDBC code needs to see certain changes within the same transaction. - * Else, it is preferable to rely on auto-flushing at transaction - * completion. - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#flush - */ - void flush() throws DataAccessException; - - /** - * Remove all objects from the {@link org.hibernate.Session} cache, and - * cancel all pending saves, updates and deletes. - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#clear - */ - void clear() throws DataAccessException; - - - //------------------------------------------------------------------------- - // Convenience finder methods for detached criteria - //------------------------------------------------------------------------- - - /** - * Execute a query based on a given Hibernate criteria object. - * @param criteria the detached Hibernate criteria object. - * Note: Do not reuse criteria objects! They need to recreated per execution, - * due to the suboptimal design of Hibernate's criteria facility. - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see DetachedCriteria#getExecutableCriteria(org.hibernate.Session) - */ - List findByCriteria(DetachedCriteria criteria) throws DataAccessException; - - /** - * Execute a query based on the given Hibernate criteria object. - * @param criteria the detached Hibernate criteria object. - * Note: Do not reuse criteria objects! They need to recreated per execution, - * due to the suboptimal design of Hibernate's criteria facility. - * @param firstResult the index of the first result object to be retrieved - * (numbered from 0) - * @param maxResults the maximum number of result objects to retrieve - * (or <=0 for no limit) - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see DetachedCriteria#getExecutableCriteria(org.hibernate.Session) - * @see org.hibernate.Criteria#setFirstResult(int) - * @see org.hibernate.Criteria#setMaxResults(int) - */ - List findByCriteria(DetachedCriteria criteria, int firstResult, int maxResults) throws DataAccessException; - - /** - * Execute a query based on the given example entity object. - * @param exampleEntity an instance of the desired entity, - * serving as example for "query-by-example" - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.criterion.Example#create(Object) - */ - List findByExample(T exampleEntity) throws DataAccessException; - - /** - * Execute a query based on the given example entity object. - * @param entityName the name of the persistent entity - * @param exampleEntity an instance of the desired entity, - * serving as example for "query-by-example" - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.criterion.Example#create(Object) - */ - List findByExample(String entityName, T exampleEntity) throws DataAccessException; - - /** - * Execute a query based on a given example entity object. - * @param exampleEntity an instance of the desired entity, - * serving as example for "query-by-example" - * @param firstResult the index of the first result object to be retrieved - * (numbered from 0) - * @param maxResults the maximum number of result objects to retrieve - * (or <=0 for no limit) - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.criterion.Example#create(Object) - * @see org.hibernate.Criteria#setFirstResult(int) - * @see org.hibernate.Criteria#setMaxResults(int) - */ - List findByExample(T exampleEntity, int firstResult, int maxResults) throws DataAccessException; - - /** - * Execute a query based on a given example entity object. - * @param entityName the name of the persistent entity - * @param exampleEntity an instance of the desired entity, - * serving as example for "query-by-example" - * @param firstResult the index of the first result object to be retrieved - * (numbered from 0) - * @param maxResults the maximum number of result objects to retrieve - * (or <=0 for no limit) - * @return a {@link List} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.criterion.Example#create(Object) - * @see org.hibernate.Criteria#setFirstResult(int) - * @see org.hibernate.Criteria#setMaxResults(int) - */ - List findByExample(String entityName, T exampleEntity, int firstResult, int maxResults) - throws DataAccessException; - - - //------------------------------------------------------------------------- - // Convenience finder methods for HQL strings - //------------------------------------------------------------------------- - - /** - * Execute an HQL query, binding a number of values to "?" parameters - * in the query string. - * @param queryString a query expressed in Hibernate's query language - * @param values the values of the parameters - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#createQuery - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List find(String queryString, Object... values) throws DataAccessException; - - /** - * Execute an HQL query, binding one value to a ":" named parameter - * in the query string. - * @param queryString a query expressed in Hibernate's query language - * @param paramName the name of the parameter - * @param value the value of the parameter - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#getNamedQuery(String) - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByNamedParam(String queryString, String paramName, Object value) throws DataAccessException; - - /** - * Execute an HQL query, binding a number of values to ":" named - * parameters in the query string. - * @param queryString a query expressed in Hibernate's query language - * @param paramNames the names of the parameters - * @param values the values of the parameters - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#getNamedQuery(String) - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByNamedParam(String queryString, String[] paramNames, Object[] values) throws DataAccessException; - - /** - * Execute an HQL query, binding the properties of the given bean to - * named parameters in the query string. - * @param queryString a query expressed in Hibernate's query language - * @param valueBean the values of the parameters - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Query#setProperties - * @see org.hibernate.Session#createQuery - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByValueBean(String queryString, Object valueBean) throws DataAccessException; - - - //------------------------------------------------------------------------- - // Convenience finder methods for named queries - //------------------------------------------------------------------------- - - /** - * Execute a named query binding a number of values to "?" parameters - * in the query string. - *

    A named query is defined in a Hibernate mapping file. - * @param queryName the name of a Hibernate query in a mapping file - * @param values the values of the parameters - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#getNamedQuery(String) - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByNamedQuery(String queryName, Object... values) throws DataAccessException; - - /** - * Execute a named query, binding one value to a ":" named parameter - * in the query string. - *

    A named query is defined in a Hibernate mapping file. - * @param queryName the name of a Hibernate query in a mapping file - * @param paramName the name of parameter - * @param value the value of the parameter - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#getNamedQuery(String) - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) - throws DataAccessException; - - /** - * Execute a named query, binding a number of values to ":" named - * parameters in the query string. - *

    A named query is defined in a Hibernate mapping file. - * @param queryName the name of a Hibernate query in a mapping file - * @param paramNames the names of the parameters - * @param values the values of the parameters - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#getNamedQuery(String) - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByNamedQueryAndNamedParam(String queryName, String[] paramNames, Object[] values) - throws DataAccessException; - - /** - * Execute a named query, binding the properties of the given bean to - * ":" named parameters in the query string. - *

    A named query is defined in a Hibernate mapping file. - * @param queryName the name of a Hibernate query in a mapping file - * @param valueBean the values of the parameters - * @return a {@link List} containing the results of the query execution - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Query#setProperties - * @see org.hibernate.Session#getNamedQuery(String) - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - List findByNamedQueryAndValueBean(String queryName, Object valueBean) throws DataAccessException; - - - //------------------------------------------------------------------------- - // Convenience query methods for iteration and bulk updates/deletes - //------------------------------------------------------------------------- - - /** - * Execute a query for persistent instances, binding a number of - * values to "?" parameters in the query string. - *

    Returns the results as an {@link Iterator}. Entities returned are - * initialized on demand. See the Hibernate API documentation for details. - * @param queryString a query expressed in Hibernate's query language - * @param values the values of the parameters - * @return an {@link Iterator} containing 0 or more persistent instances - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#createQuery - * @see org.hibernate.Query#iterate - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - Iterator iterate(String queryString, Object... values) throws DataAccessException; - - /** - * Immediately close an {@link Iterator} created by any of the various - * {@code iterate(..)} operations, instead of waiting until the - * session is closed or disconnected. - * @param it the {@code Iterator} to close - * @throws DataAccessException if the {@code Iterator} could not be closed - * @see org.hibernate.Hibernate#close - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - void closeIterator(Iterator it) throws DataAccessException; - - /** - * Update/delete all objects according to the given query, binding a number of - * values to "?" parameters in the query string. - * @param queryString an update/delete query expressed in Hibernate's query language - * @param values the values of the parameters - * @return the number of instances updated/deleted - * @throws DataAccessException in case of Hibernate errors - * @see org.hibernate.Session#createQuery - * @see org.hibernate.Query#executeUpdate - * @deprecated as of 5.0.4, in favor of a custom {@link HibernateCallback} - * lambda code block passed to the general {@link #execute} method - */ - @Deprecated - int bulkUpdate(String queryString, Object... values) throws DataAccessException; - -} diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java index 0246df5a2818..c3bbfe98a33d 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateOptimisticLockingFailureException.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ public class HibernateOptimisticLockingFailureException extends ObjectOptimisticLockingFailureException { public HibernateOptimisticLockingFailureException(StaleObjectStateException ex) { - super(ex.getEntityName(), HibernateObjectRetrievalFailureException.getIdentifier(ex), ex.getMessage(), ex); + super(ex.getEntityName(), ex.getIdentifier(), ex.getMessage(), ex); } public HibernateOptimisticLockingFailureException(StaleStateException ex) { diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java index 1122c91a2498..bbae833ab4fc 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateQueryException.java @@ -17,9 +17,9 @@ package org.springframework.orm.hibernate5; import org.hibernate.QueryException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.lang.Nullable; /** * Hibernate-specific subclass of InvalidDataAccessResourceUsageException, @@ -39,10 +39,9 @@ public HibernateQueryException(QueryException ex) { /** * Return the HQL query string that was invalid. */ - @Nullable - @SuppressWarnings("NullAway") - public String getQueryString() { - return ((QueryException) getCause()).getQueryString(); + public @Nullable String getQueryString() { + QueryException cause = (QueryException) getCause(); + return cause == null ? null : cause.getQueryString(); } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java index 5831fa0ba4f0..fba045573dc0 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateSystemException.java @@ -17,9 +17,9 @@ package org.springframework.orm.hibernate5; import org.hibernate.HibernateException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; /** * Hibernate-specific subclass of UncategorizedDataAccessException, diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java deleted file mode 100644 index 51ef7fd4620b..000000000000 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTemplate.java +++ /dev/null @@ -1,1185 +0,0 @@ -/* - * Copyright 2002-2023 the original author 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.orm.hibernate5; - -import java.io.Serializable; -import java.lang.reflect.InvocationHandler; -import java.lang.reflect.InvocationTargetException; -import java.lang.reflect.Method; -import java.lang.reflect.Proxy; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; - -import jakarta.persistence.PersistenceException; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.hibernate.Criteria; -import org.hibernate.Filter; -import org.hibernate.FlushMode; -import org.hibernate.Hibernate; -import org.hibernate.HibernateException; -import org.hibernate.LockMode; -import org.hibernate.LockOptions; -import org.hibernate.ReplicationMode; -import org.hibernate.Session; -import org.hibernate.SessionFactory; -import org.hibernate.criterion.DetachedCriteria; -import org.hibernate.criterion.Example; -import org.hibernate.query.Query; - -import org.springframework.beans.factory.InitializingBean; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; -import org.springframework.transaction.support.ResourceHolderSupport; -import org.springframework.transaction.support.TransactionSynchronizationManager; -import org.springframework.util.Assert; - -/** - * Helper class that simplifies Hibernate data access code. Automatically - * converts HibernateExceptions into DataAccessExceptions, following the - * {@code org.springframework.dao} exception hierarchy. - * - *

    The central method is {@code execute}, supporting Hibernate access code - * implementing the {@link HibernateCallback} interface. It provides Hibernate Session - * handling such that neither the HibernateCallback implementation nor the calling - * code needs to explicitly care about retrieving/closing Hibernate Sessions, - * or handling Session lifecycle exceptions. For typical single step actions, - * there are various convenience methods (find, load, saveOrUpdate, delete). - * - *

    Can be used within a service implementation via direct instantiation - * with a SessionFactory reference, or get prepared in an application context - * and given to services as bean reference. Note: The SessionFactory should - * always be configured as bean in the application context, in the first case - * given to the service directly, in the second case to the prepared template. - * - *

    NOTE: Hibernate access code can also be coded against the native Hibernate - * {@link Session}. Hence, for newly started projects, consider adopting the standard - * Hibernate style of coding against {@link SessionFactory#getCurrentSession()}. - * Alternatively, use {@link #execute(HibernateCallback)} with Java 8 lambda code blocks - * against the callback-provided {@code Session} which results in elegant code as well, - * decoupled from the Hibernate Session lifecycle. The remaining operations on this - * HibernateTemplate are deprecated in the meantime and primarily exist as a migration - * helper for older Hibernate 3.x/4.x data access code in existing applications. - * - * @author Juergen Hoeller - * @since 4.2 - * @see #setSessionFactory - * @see HibernateCallback - * @see Session - * @see LocalSessionFactoryBean - * @see HibernateTransactionManager - * @see org.springframework.orm.hibernate5.support.OpenSessionInViewFilter - * @see org.springframework.orm.hibernate5.support.OpenSessionInViewInterceptor - */ -public class HibernateTemplate implements HibernateOperations, InitializingBean { - - protected final Log logger = LogFactory.getLog(getClass()); - - @Nullable - private SessionFactory sessionFactory; - - @Nullable - private String[] filterNames; - - private boolean exposeNativeSession = false; - - private boolean checkWriteOperations = true; - - private boolean cacheQueries = false; - - @Nullable - private String queryCacheRegion; - - private int fetchSize = 0; - - private int maxResults = 0; - - - /** - * Create a new HibernateTemplate instance. - */ - public HibernateTemplate() { - } - - /** - * Create a new HibernateTemplate instance. - * @param sessionFactory the SessionFactory to create Sessions with - */ - public HibernateTemplate(SessionFactory sessionFactory) { - setSessionFactory(sessionFactory); - afterPropertiesSet(); - } - - - /** - * Set the Hibernate SessionFactory that should be used to create - * Hibernate Sessions. - */ - public void setSessionFactory(@Nullable SessionFactory sessionFactory) { - this.sessionFactory = sessionFactory; - } - - /** - * Return the Hibernate SessionFactory that should be used to create - * Hibernate Sessions. - */ - @Nullable - public SessionFactory getSessionFactory() { - return this.sessionFactory; - } - - /** - * Obtain the SessionFactory for actual use. - * @return the SessionFactory (never {@code null}) - * @throws IllegalStateException in case of no SessionFactory set - * @since 5.0 - */ - protected final SessionFactory obtainSessionFactory() { - SessionFactory sessionFactory = getSessionFactory(); - Assert.state(sessionFactory != null, "No SessionFactory set"); - return sessionFactory; - } - - /** - * Set one or more names of Hibernate filters to be activated for all - * Sessions that this accessor works with. - *

    Each of those filters will be enabled at the beginning of each - * operation and correspondingly disabled at the end of the operation. - * This will work for newly opened Sessions as well as for existing - * Sessions (for example, within a transaction). - * @see #enableFilters(Session) - * @see Session#enableFilter(String) - */ - public void setFilterNames(@Nullable String... filterNames) { - this.filterNames = filterNames; - } - - /** - * Return the names of Hibernate filters to be activated, if any. - */ - @Nullable - public String[] getFilterNames() { - return this.filterNames; - } - - /** - * Set whether to expose the native Hibernate Session to - * HibernateCallback code. - *

    Default is "false": a Session proxy will be returned, suppressing - * {@code close} calls and automatically applying query cache - * settings and transaction timeouts. - * @see HibernateCallback - * @see Session - * @see #setCacheQueries - * @see #setQueryCacheRegion - * @see #prepareQuery - * @see #prepareCriteria - */ - public void setExposeNativeSession(boolean exposeNativeSession) { - this.exposeNativeSession = exposeNativeSession; - } - - /** - * Return whether to expose the native Hibernate Session to - * HibernateCallback code, or rather a Session proxy. - */ - public boolean isExposeNativeSession() { - return this.exposeNativeSession; - } - - /** - * Set whether to check that the Hibernate Session is not in read-only mode - * in case of write operations (save/update/delete). - *

    Default is "true", for fail-fast behavior when attempting write operations - * within a read-only transaction. Turn this off to allow save/update/delete - * on a Session with flush mode MANUAL. - * @see #checkWriteOperationAllowed - * @see org.springframework.transaction.TransactionDefinition#isReadOnly - */ - public void setCheckWriteOperations(boolean checkWriteOperations) { - this.checkWriteOperations = checkWriteOperations; - } - - /** - * Return whether to check that the Hibernate Session is not in read-only - * mode in case of write operations (save/update/delete). - */ - public boolean isCheckWriteOperations() { - return this.checkWriteOperations; - } - - /** - * Set whether to cache all queries executed by this template. - *

    If this is "true", all Query and Criteria objects created by - * this template will be marked as cacheable (including all - * queries through find methods). - *

    To specify the query region to be used for queries cached - * by this template, set the "queryCacheRegion" property. - * @see #setQueryCacheRegion - * @see Query#setCacheable - * @see Criteria#setCacheable - */ - public void setCacheQueries(boolean cacheQueries) { - this.cacheQueries = cacheQueries; - } - - /** - * Return whether to cache all queries executed by this template. - */ - public boolean isCacheQueries() { - return this.cacheQueries; - } - - /** - * Set the name of the cache region for queries executed by this template. - *

    If this is specified, it will be applied to all Query and Criteria objects - * created by this template (including all queries through find methods). - *

    The cache region will not take effect unless queries created by this - * template are configured to be cached via the "cacheQueries" property. - * @see #setCacheQueries - * @see Query#setCacheRegion - * @see Criteria#setCacheRegion - */ - public void setQueryCacheRegion(@Nullable String queryCacheRegion) { - this.queryCacheRegion = queryCacheRegion; - } - - /** - * Return the name of the cache region for queries executed by this template. - */ - @Nullable - public String getQueryCacheRegion() { - return this.queryCacheRegion; - } - - /** - * Set the fetch size for this HibernateTemplate. This is important for processing - * large result sets: Setting this higher than the default value will increase - * processing speed at the cost of memory consumption; setting this lower can - * avoid transferring row data that will never be read by the application. - *

    Default is 0, indicating to use the JDBC driver's default. - */ - public void setFetchSize(int fetchSize) { - this.fetchSize = fetchSize; - } - - /** - * Return the fetch size specified for this HibernateTemplate. - */ - public int getFetchSize() { - return this.fetchSize; - } - - /** - * Set the maximum number of rows for this HibernateTemplate. This is important - * for processing subsets of large result sets, avoiding to read and hold - * the entire result set in the database or in the JDBC driver if we're - * never interested in the entire result in the first place (for example, - * when performing searches that might return a large number of matches). - *

    Default is 0, indicating to use the JDBC driver's default. - */ - public void setMaxResults(int maxResults) { - this.maxResults = maxResults; - } - - /** - * Return the maximum number of rows specified for this HibernateTemplate. - */ - public int getMaxResults() { - return this.maxResults; - } - - @Override - public void afterPropertiesSet() { - if (getSessionFactory() == null) { - throw new IllegalArgumentException("Property 'sessionFactory' is required"); - } - } - - - @Override - @Nullable - public T execute(HibernateCallback action) throws DataAccessException { - return doExecute(action, false); - } - - /** - * Execute the action specified by the given action object within a - * native {@link Session}. - *

    This execute variant overrides the template-wide - * {@link #isExposeNativeSession() "exposeNativeSession"} setting. - * @param action callback object that specifies the Hibernate action - * @return a result object returned by the action, or {@code null} - * @throws DataAccessException in case of Hibernate errors - */ - @Nullable - public T executeWithNativeSession(HibernateCallback action) { - return doExecute(action, true); - } - - /** - * Execute the action specified by the given action object within a Session. - * @param action callback object that specifies the Hibernate action - * @param enforceNativeSession whether to enforce exposure of the native - * Hibernate Session to callback code - * @return a result object returned by the action, or {@code null} - * @throws DataAccessException in case of Hibernate errors - */ - @Nullable - protected T doExecute(HibernateCallback action, boolean enforceNativeSession) throws DataAccessException { - Assert.notNull(action, "Callback object must not be null"); - - Session session = null; - boolean isNew = false; - try { - session = obtainSessionFactory().getCurrentSession(); - } - catch (HibernateException ex) { - logger.debug("Could not retrieve pre-bound Hibernate session", ex); - } - if (session == null) { - session = obtainSessionFactory().openSession(); - session.setHibernateFlushMode(FlushMode.MANUAL); - isNew = true; - } - - try { - enableFilters(session); - Session sessionToExpose = - (enforceNativeSession || isExposeNativeSession() ? session : createSessionProxy(session)); - return action.doInHibernate(sessionToExpose); - } - catch (HibernateException ex) { - throw SessionFactoryUtils.convertHibernateAccessException(ex); - } - catch (PersistenceException ex) { - if (ex.getCause() instanceof HibernateException hibernateEx) { - throw SessionFactoryUtils.convertHibernateAccessException(hibernateEx); - } - throw ex; - } - catch (RuntimeException ex) { - // Callback code threw application exception... - throw ex; - } - finally { - if (isNew) { - SessionFactoryUtils.closeSession(session); - } - else { - disableFilters(session); - } - } - } - - /** - * Create a close-suppressing proxy for the given Hibernate Session. - * The proxy also prepares returned Query and Criteria objects. - * @param session the Hibernate Session to create a proxy for - * @return the Session proxy - * @see Session#close() - * @see #prepareQuery - * @see #prepareCriteria - */ - protected Session createSessionProxy(Session session) { - return (Session) Proxy.newProxyInstance( - session.getClass().getClassLoader(), new Class[] {Session.class}, - new CloseSuppressingInvocationHandler(session)); - } - - /** - * Enable the specified filters on the given Session. - * @param session the current Hibernate Session - * @see #setFilterNames - * @see Session#enableFilter(String) - */ - protected void enableFilters(Session session) { - String[] filterNames = getFilterNames(); - if (filterNames != null) { - for (String filterName : filterNames) { - session.enableFilter(filterName); - } - } - } - - /** - * Disable the specified filters on the given Session. - * @param session the current Hibernate Session - * @see #setFilterNames - * @see Session#disableFilter(String) - */ - protected void disableFilters(Session session) { - String[] filterNames = getFilterNames(); - if (filterNames != null) { - for (String filterName : filterNames) { - session.disableFilter(filterName); - } - } - } - - - //------------------------------------------------------------------------- - // Convenience methods for loading individual objects - //------------------------------------------------------------------------- - - @Override - @Nullable - public T get(Class entityClass, Serializable id) throws DataAccessException { - return get(entityClass, id, null); - } - - @Override - @Nullable - public T get(Class entityClass, Serializable id, @Nullable LockMode lockMode) throws DataAccessException { - return executeWithNativeSession(session -> { - if (lockMode != null) { - return session.get(entityClass, id, new LockOptions(lockMode)); - } - else { - return session.get(entityClass, id); - } - }); - } - - @Override - @Nullable - public Object get(String entityName, Serializable id) throws DataAccessException { - return get(entityName, id, null); - } - - @Override - @Nullable - public Object get(String entityName, Serializable id, @Nullable LockMode lockMode) throws DataAccessException { - return executeWithNativeSession(session -> { - if (lockMode != null) { - return session.get(entityName, id, new LockOptions(lockMode)); - } - else { - return session.get(entityName, id); - } - }); - } - - @Override - public T load(Class entityClass, Serializable id) throws DataAccessException { - return load(entityClass, id, null); - } - - @Override - public T load(Class entityClass, Serializable id, @Nullable LockMode lockMode) - throws DataAccessException { - - return nonNull(executeWithNativeSession(session -> { - if (lockMode != null) { - return session.load(entityClass, id, new LockOptions(lockMode)); - } - else { - return session.load(entityClass, id); - } - })); - } - - @Override - public Object load(String entityName, Serializable id) throws DataAccessException { - return load(entityName, id, null); - } - - @Override - public Object load(String entityName, Serializable id, @Nullable LockMode lockMode) throws DataAccessException { - return nonNull(executeWithNativeSession(session -> { - if (lockMode != null) { - return session.load(entityName, id, new LockOptions(lockMode)); - } - else { - return session.load(entityName, id); - } - })); - } - - @Override - @SuppressWarnings({"unchecked", "deprecation"}) - public List loadAll(Class entityClass) throws DataAccessException { - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Criteria criteria = session.createCriteria(entityClass); - criteria.setResultTransformer(Criteria.DISTINCT_ROOT_ENTITY); - prepareCriteria(criteria); - return criteria.list(); - })); - } - - @Override - public void load(Object entity, Serializable id) throws DataAccessException { - executeWithNativeSession(session -> { - session.load(entity, id); - return null; - }); - } - - @Override - public void refresh(Object entity) throws DataAccessException { - refresh(entity, null); - } - - @Override - public void refresh(Object entity, @Nullable LockMode lockMode) throws DataAccessException { - executeWithNativeSession(session -> { - if (lockMode != null) { - session.refresh(entity, new LockOptions(lockMode)); - } - else { - session.refresh(entity); - } - return null; - }); - } - - @Override - public boolean contains(Object entity) throws DataAccessException { - Boolean result = executeWithNativeSession(session -> session.contains(entity)); - Assert.state(result != null, "No contains result"); - return result; - } - - @Override - public void evict(Object entity) throws DataAccessException { - executeWithNativeSession(session -> { - session.evict(entity); - return null; - }); - } - - @Override - public void initialize(Object proxy) throws DataAccessException { - try { - Hibernate.initialize(proxy); - } - catch (HibernateException ex) { - throw SessionFactoryUtils.convertHibernateAccessException(ex); - } - } - - @Override - public Filter enableFilter(String filterName) throws IllegalStateException { - Session session = obtainSessionFactory().getCurrentSession(); - Filter filter = session.getEnabledFilter(filterName); - if (filter == null) { - filter = session.enableFilter(filterName); - } - return filter; - } - - - //------------------------------------------------------------------------- - // Convenience methods for storing individual objects - //------------------------------------------------------------------------- - - @Override - public void lock(Object entity, LockMode lockMode) throws DataAccessException { - executeWithNativeSession(session -> { - session.buildLockRequest(new LockOptions(lockMode)).lock(entity); - return null; - }); - } - - @Override - public void lock(String entityName, Object entity, LockMode lockMode) - throws DataAccessException { - - executeWithNativeSession(session -> { - session.buildLockRequest(new LockOptions(lockMode)).lock(entityName, entity); - return null; - }); - } - - @Override - public Serializable save(Object entity) throws DataAccessException { - return nonNull(executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - return session.save(entity); - })); - } - - @Override - public Serializable save(String entityName, Object entity) throws DataAccessException { - return nonNull(executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - return session.save(entityName, entity); - })); - } - - @Override - public void update(Object entity) throws DataAccessException { - update(entity, null); - } - - @Override - public void update(Object entity, @Nullable LockMode lockMode) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.update(entity); - if (lockMode != null) { - session.buildLockRequest(new LockOptions(lockMode)).lock(entity); - } - return null; - }); - } - - @Override - public void update(String entityName, Object entity) throws DataAccessException { - update(entityName, entity, null); - } - - @Override - public void update(String entityName, Object entity, @Nullable LockMode lockMode) - throws DataAccessException { - - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.update(entityName, entity); - if (lockMode != null) { - session.buildLockRequest(new LockOptions(lockMode)).lock(entityName, entity); - } - return null; - }); - } - - @Override - public void saveOrUpdate(Object entity) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.saveOrUpdate(entity); - return null; - }); - } - - @Override - public void saveOrUpdate(String entityName, Object entity) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.saveOrUpdate(entityName, entity); - return null; - }); - } - - @Override - public void replicate(Object entity, ReplicationMode replicationMode) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.replicate(entity, replicationMode); - return null; - }); - } - - @Override - public void replicate(String entityName, Object entity, ReplicationMode replicationMode) - throws DataAccessException { - - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.replicate(entityName, entity, replicationMode); - return null; - }); - } - - @Override - public void persist(Object entity) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.persist(entity); - return null; - }); - } - - @Override - public void persist(String entityName, Object entity) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - session.persist(entityName, entity); - return null; - }); - } - - @Override - @SuppressWarnings("unchecked") - public T merge(T entity) throws DataAccessException { - return nonNull(executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - return (T) session.merge(entity); - })); - } - - @Override - @SuppressWarnings("unchecked") - public T merge(String entityName, T entity) throws DataAccessException { - return nonNull(executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - return (T) session.merge(entityName, entity); - })); - } - - @Override - public void delete(Object entity) throws DataAccessException { - delete(entity, null); - } - - @Override - public void delete(Object entity, @Nullable LockMode lockMode) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - if (lockMode != null) { - session.buildLockRequest(new LockOptions(lockMode)).lock(entity); - } - session.delete(entity); - return null; - }); - } - - @Override - public void delete(String entityName, Object entity) throws DataAccessException { - delete(entityName, entity, null); - } - - @Override - public void delete(String entityName, Object entity, @Nullable LockMode lockMode) - throws DataAccessException { - - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - if (lockMode != null) { - session.buildLockRequest(new LockOptions(lockMode)).lock(entityName, entity); - } - session.delete(entityName, entity); - return null; - }); - } - - @Override - public void deleteAll(Collection entities) throws DataAccessException { - executeWithNativeSession(session -> { - checkWriteOperationAllowed(session); - for (Object entity : entities) { - session.delete(entity); - } - return null; - }); - } - - @Override - public void flush() throws DataAccessException { - executeWithNativeSession(session -> { - session.flush(); - return null; - }); - } - - @Override - public void clear() throws DataAccessException { - executeWithNativeSession(session -> { - session.clear(); - return null; - }); - } - - - //------------------------------------------------------------------------- - // Convenience finder methods for detached criteria - //------------------------------------------------------------------------- - - @Override - public List findByCriteria(DetachedCriteria criteria) throws DataAccessException { - return findByCriteria(criteria, -1, -1); - } - - @Override - public List findByCriteria(DetachedCriteria criteria, int firstResult, int maxResults) - throws DataAccessException { - - Assert.notNull(criteria, "DetachedCriteria must not be null"); - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Criteria executableCriteria = criteria.getExecutableCriteria(session); - prepareCriteria(executableCriteria); - if (firstResult >= 0) { - executableCriteria.setFirstResult(firstResult); - } - if (maxResults > 0) { - executableCriteria.setMaxResults(maxResults); - } - return executableCriteria.list(); - })); - } - - @Override - public List findByExample(T exampleEntity) throws DataAccessException { - return findByExample(null, exampleEntity, -1, -1); - } - - @Override - public List findByExample(String entityName, T exampleEntity) throws DataAccessException { - return findByExample(entityName, exampleEntity, -1, -1); - } - - @Override - public List findByExample(T exampleEntity, int firstResult, int maxResults) throws DataAccessException { - return findByExample(null, exampleEntity, firstResult, maxResults); - } - - @Override - @SuppressWarnings({"unchecked", "deprecation"}) - public List findByExample(@Nullable String entityName, T exampleEntity, int firstResult, int maxResults) - throws DataAccessException { - - Assert.notNull(exampleEntity, "Example entity must not be null"); - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Criteria executableCriteria = (entityName != null ? - session.createCriteria(entityName) : session.createCriteria(exampleEntity.getClass())); - executableCriteria.add(Example.create(exampleEntity)); - prepareCriteria(executableCriteria); - if (firstResult >= 0) { - executableCriteria.setFirstResult(firstResult); - } - if (maxResults > 0) { - executableCriteria.setMaxResults(maxResults); - } - return executableCriteria.list(); - })); - } - - - //------------------------------------------------------------------------- - // Convenience finder methods for HQL strings - //------------------------------------------------------------------------- - - @Deprecated - @Override - public List find(String queryString, @Nullable Object... values) throws DataAccessException { - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.createQuery(queryString); - prepareQuery(queryObject); - if (values != null) { - for (int i = 0; i < values.length; i++) { - queryObject.setParameter(i, values[i]); - } - } - return queryObject.list(); - })); - } - - @Deprecated - @Override - public List findByNamedParam(String queryString, String paramName, Object value) - throws DataAccessException { - - return findByNamedParam(queryString, new String[] {paramName}, new Object[] {value}); - } - - @Deprecated - @Override - public List findByNamedParam(String queryString, String[] paramNames, Object[] values) - throws DataAccessException { - - if (paramNames.length != values.length) { - throw new IllegalArgumentException("Length of paramNames array must match length of values array"); - } - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.createQuery(queryString); - prepareQuery(queryObject); - for (int i = 0; i < values.length; i++) { - applyNamedParameterToQuery(queryObject, paramNames[i], values[i]); - } - return queryObject.list(); - })); - } - - @Deprecated - @Override - public List findByValueBean(String queryString, Object valueBean) throws DataAccessException { - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.createQuery(queryString); - prepareQuery(queryObject); - queryObject.setProperties(valueBean); - return queryObject.list(); - })); - } - - - //------------------------------------------------------------------------- - // Convenience finder methods for named queries - //------------------------------------------------------------------------- - - @Deprecated - @Override - public List findByNamedQuery(String queryName, @Nullable Object... values) throws DataAccessException { - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.getNamedQuery(queryName); - prepareQuery(queryObject); - if (values != null) { - for (int i = 0; i < values.length; i++) { - queryObject.setParameter(i, values[i]); - } - } - return queryObject.list(); - })); - } - - @Deprecated - @Override - public List findByNamedQueryAndNamedParam(String queryName, String paramName, Object value) - throws DataAccessException { - - return findByNamedQueryAndNamedParam(queryName, new String[] {paramName}, new Object[] {value}); - } - - @Deprecated - @Override - @SuppressWarnings("NullAway") - public List findByNamedQueryAndNamedParam( - String queryName, @Nullable String[] paramNames, @Nullable Object[] values) - throws DataAccessException { - - if (values != null && (paramNames == null || paramNames.length != values.length)) { - throw new IllegalArgumentException("Length of paramNames array must match length of values array"); - } - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.getNamedQuery(queryName); - prepareQuery(queryObject); - if (values != null) { - for (int i = 0; i < values.length; i++) { - applyNamedParameterToQuery(queryObject, paramNames[i], values[i]); - } - } - return queryObject.list(); - })); - } - - @Deprecated - @Override - public List findByNamedQueryAndValueBean(String queryName, Object valueBean) throws DataAccessException { - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.getNamedQuery(queryName); - prepareQuery(queryObject); - queryObject.setProperties(valueBean); - return queryObject.list(); - })); - } - - - //------------------------------------------------------------------------- - // Convenience query methods for iteration and bulk updates/deletes - //------------------------------------------------------------------------- - - @SuppressWarnings("deprecation") - @Deprecated - @Override - public Iterator iterate(String queryString, @Nullable Object... values) throws DataAccessException { - return nonNull(executeWithNativeSession((HibernateCallback>) session -> { - Query queryObject = session.createQuery(queryString); - prepareQuery(queryObject); - if (values != null) { - for (int i = 0; i < values.length; i++) { - queryObject.setParameter(i, values[i]); - } - } - return queryObject.iterate(); - })); - } - - @Deprecated - @Override - public void closeIterator(Iterator it) throws DataAccessException { - try { - Hibernate.close(it); - } - catch (HibernateException ex) { - throw SessionFactoryUtils.convertHibernateAccessException(ex); - } - } - - @Deprecated - @Override - public int bulkUpdate(String queryString, @Nullable Object... values) throws DataAccessException { - Integer result = executeWithNativeSession(session -> { - Query queryObject = session.createQuery(queryString); - prepareQuery(queryObject); - if (values != null) { - for (int i = 0; i < values.length; i++) { - queryObject.setParameter(i, values[i]); - } - } - return queryObject.executeUpdate(); - }); - Assert.state(result != null, "No update count"); - return result; - } - - - //------------------------------------------------------------------------- - // Helper methods used by the operations above - //------------------------------------------------------------------------- - - /** - * Check whether write operations are allowed on the given Session. - *

    Default implementation throws an InvalidDataAccessApiUsageException in - * case of {@code FlushMode.MANUAL}. Can be overridden in subclasses. - * @param session current Hibernate Session - * @throws InvalidDataAccessApiUsageException if write operations are not allowed - * @see #setCheckWriteOperations - * @see Session#getFlushMode() - * @see FlushMode#MANUAL - */ - protected void checkWriteOperationAllowed(Session session) throws InvalidDataAccessApiUsageException { - if (isCheckWriteOperations() && session.getHibernateFlushMode().lessThan(FlushMode.COMMIT)) { - throw new InvalidDataAccessApiUsageException( - "Write operations are not allowed in read-only mode (FlushMode.MANUAL): "+ - "Turn your Session into FlushMode.COMMIT/AUTO or remove 'readOnly' marker from transaction definition."); - } - } - - /** - * Prepare the given Criteria object, applying cache settings and/or - * a transaction timeout. - * @param criteria the Criteria object to prepare - * @see #setCacheQueries - * @see #setQueryCacheRegion - */ - protected void prepareCriteria(Criteria criteria) { - if (isCacheQueries()) { - criteria.setCacheable(true); - if (getQueryCacheRegion() != null) { - criteria.setCacheRegion(getQueryCacheRegion()); - } - } - if (getFetchSize() > 0) { - criteria.setFetchSize(getFetchSize()); - } - if (getMaxResults() > 0) { - criteria.setMaxResults(getMaxResults()); - } - - ResourceHolderSupport sessionHolder = - (ResourceHolderSupport) TransactionSynchronizationManager.getResource(obtainSessionFactory()); - if (sessionHolder != null && sessionHolder.hasTimeout()) { - criteria.setTimeout(sessionHolder.getTimeToLiveInSeconds()); - } - } - - /** - * Prepare the given Query object, applying cache settings and/or - * a transaction timeout. - * @param queryObject the Query object to prepare - * @see #setCacheQueries - * @see #setQueryCacheRegion - */ - protected void prepareQuery(Query queryObject) { - if (isCacheQueries()) { - queryObject.setCacheable(true); - if (getQueryCacheRegion() != null) { - queryObject.setCacheRegion(getQueryCacheRegion()); - } - } - if (getFetchSize() > 0) { - queryObject.setFetchSize(getFetchSize()); - } - if (getMaxResults() > 0) { - queryObject.setMaxResults(getMaxResults()); - } - - ResourceHolderSupport sessionHolder = - (ResourceHolderSupport) TransactionSynchronizationManager.getResource(obtainSessionFactory()); - if (sessionHolder != null && sessionHolder.hasTimeout()) { - queryObject.setTimeout(sessionHolder.getTimeToLiveInSeconds()); - } - } - - /** - * Apply the given name parameter to the given Query object. - * @param queryObject the Query object - * @param paramName the name of the parameter - * @param value the value of the parameter - * @throws HibernateException if thrown by the Query object - */ - protected void applyNamedParameterToQuery(Query queryObject, String paramName, Object value) - throws HibernateException { - - if (value instanceof Collection collection) { - queryObject.setParameterList(paramName, collection); - } - else if (value instanceof Object[] array) { - queryObject.setParameterList(paramName, array); - } - else { - queryObject.setParameter(paramName, value); - } - } - - private static T nonNull(@Nullable T result) { - Assert.state(result != null, "No result"); - return result; - } - - - /** - * Invocation handler that suppresses close calls on Hibernate Sessions. - * Also prepares returned Query and Criteria objects. - * @see Session#close - */ - private class CloseSuppressingInvocationHandler implements InvocationHandler { - - private final Session target; - - public CloseSuppressingInvocationHandler(Session target) { - this.target = target; - } - - @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { - // Invocation on Session interface coming in... - - return switch (method.getName()) { - // Only consider equal when proxies are identical. - case "equals" -> (proxy == args[0]); - // Use hashCode of Session proxy. - case "hashCode" -> System.identityHashCode(proxy); - // Handle close method: suppress, not valid. - case "close" -> null; - default -> { - try { - // Invoke method on target Session. - Object retVal = method.invoke(this.target, args); - - // If return value is a Query or Criteria, apply transaction timeout. - // Applies to createQuery, getNamedQuery, createCriteria. - if (retVal instanceof Criteria criteria) { - prepareCriteria(criteria); - } - else if (retVal instanceof Query query) { - prepareQuery(query); - } - - yield retVal; - } - catch (InvocationTargetException ex) { - throw ex.getTargetException(); - } - } - }; - } - } - -} diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java index fd52e4e4e280..798ddbbad38d 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/HibernateTransactionManager.java @@ -32,6 +32,7 @@ import org.hibernate.Transaction; import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.resource.transaction.spi.TransactionStatus; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -43,7 +44,6 @@ import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; -import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.InvalidIsolationLevelException; @@ -114,11 +114,9 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, BeanFactoryAware, InitializingBean { - @Nullable - private SessionFactory sessionFactory; + private @Nullable SessionFactory sessionFactory; - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; private boolean autodetectDataSource = true; @@ -128,18 +126,15 @@ public class HibernateTransactionManager extends AbstractPlatformTransactionMana private boolean hibernateManagedSession = false; - @Nullable - private Consumer sessionInitializer; + private @Nullable Consumer sessionInitializer; - @Nullable - private Object entityInterceptor; + private @Nullable Object entityInterceptor; /** * Just needed for entityInterceptorBeanName. * @see #setEntityInterceptorBeanName */ - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** @@ -170,8 +165,7 @@ public void setSessionFactory(@Nullable SessionFactory sessionFactory) { /** * Return the SessionFactory that this instance should manage transactions for. */ - @Nullable - public SessionFactory getSessionFactory() { + public @Nullable SessionFactory getSessionFactory() { return this.sessionFactory; } @@ -231,8 +225,7 @@ public void setDataSource(@Nullable DataSource dataSource) { /** * Return the JDBC DataSource that this instance manages transactions for. */ - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { return this.dataSource; } @@ -363,8 +356,7 @@ public void setEntityInterceptor(@Nullable Interceptor entityInterceptor) { * @see #setEntityInterceptorBeanName * @see #setBeanFactory */ - @Nullable - public Interceptor getEntityInterceptor() throws IllegalStateException, BeansException { + public @Nullable Interceptor getEntityInterceptor() throws IllegalStateException, BeansException { if (this.entityInterceptor instanceof Interceptor interceptor) { return interceptor; } @@ -771,10 +763,9 @@ protected void doCleanupAfterCompletion(Object transaction) { /** * Disconnect a pre-existing Hibernate Session on transaction completion, * returning its database connection but preserving its entity state. - *

    The default implementation calls the equivalent of {@link Session#disconnect()}. - * Subclasses may override this with a no-op or with fine-tuned disconnection logic. + *

    The default implementation triggers a manual disconnect. Subclasses + * may override this with a no-op or with fine-tuned disconnection logic. * @param session the Hibernate Session to disconnect - * @see Session#disconnect() */ protected void disconnectOnCompletion(Session session) { if (session instanceof SessionImplementor sessionImpl) { @@ -800,8 +791,7 @@ protected DataAccessException convertHibernateAccessException(HibernateException */ private class HibernateTransactionObject extends JdbcTransactionObjectSupport { - @Nullable - private SessionHolder sessionHolder; + private @Nullable SessionHolder sessionHolder; private boolean newSessionHolder; @@ -809,8 +799,7 @@ private class HibernateTransactionObject extends JdbcTransactionObjectSupport { private boolean needsConnectionReset; - @Nullable - private Integer previousHoldability; + private @Nullable Integer previousHoldability; public void setSession(Session session) { this.sessionHolder = new SessionHolder(session); @@ -859,8 +848,7 @@ public void setPreviousHoldability(@Nullable Integer previousHoldability) { this.previousHoldability = previousHoldability; } - @Nullable - public Integer getPreviousHoldability() { + public @Nullable Integer getPreviousHoldability() { return this.previousHoldability; } @@ -912,8 +900,7 @@ private static final class SuspendedResourcesHolder { private final SessionHolder sessionHolder; - @Nullable - private final ConnectionHolder connectionHolder; + private final @Nullable ConnectionHolder connectionHolder; private SuspendedResourcesHolder(SessionHolder sessionHolder, @Nullable ConnectionHolder conHolder) { this.sessionHolder = sessionHolder; @@ -924,8 +911,7 @@ private SessionHolder getSessionHolder() { return this.sessionHolder; } - @Nullable - private ConnectionHolder getConnectionHolder() { + private @Nullable ConnectionHolder getConnectionHolder() { return this.connectionHolder; } } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java index 94517a0a2a04..c810de1a150c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBean.java @@ -34,6 +34,7 @@ import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.integrator.spi.Integrator; import org.hibernate.service.ServiceRegistry; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; @@ -52,7 +53,6 @@ import org.springframework.core.io.support.ResourcePatternUtils; import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.lang.Nullable; /** * {@link FactoryBean} that creates a Hibernate {@link SessionFactory}. This is the usual @@ -84,85 +84,59 @@ public class LocalSessionFactoryBean extends HibernateExceptionTranslator implements FactoryBean, ResourceLoaderAware, BeanFactoryAware, InitializingBean, SmartInitializingSingleton, DisposableBean { - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; - @Nullable - private Resource[] configLocations; + private Resource @Nullable [] configLocations; - @Nullable - private String[] mappingResources; + private String @Nullable [] mappingResources; - @Nullable - private Resource[] mappingLocations; + private Resource @Nullable [] mappingLocations; - @Nullable - private Resource[] cacheableMappingLocations; + private Resource @Nullable [] cacheableMappingLocations; - @Nullable - private Resource[] mappingJarLocations; + private Resource @Nullable [] mappingJarLocations; - @Nullable - private Resource[] mappingDirectoryLocations; + private Resource @Nullable [] mappingDirectoryLocations; - @Nullable - private Interceptor entityInterceptor; + private @Nullable Interceptor entityInterceptor; - @Nullable - private ImplicitNamingStrategy implicitNamingStrategy; + private @Nullable ImplicitNamingStrategy implicitNamingStrategy; - @Nullable - private PhysicalNamingStrategy physicalNamingStrategy; + private @Nullable PhysicalNamingStrategy physicalNamingStrategy; - @Nullable - private Object jtaTransactionManager; + private @Nullable Object jtaTransactionManager; - @Nullable - private RegionFactory cacheRegionFactory; + private @Nullable RegionFactory cacheRegionFactory; - @Nullable - private MultiTenantConnectionProvider multiTenantConnectionProvider; + private @Nullable MultiTenantConnectionProvider multiTenantConnectionProvider; - @Nullable - private CurrentTenantIdentifierResolver currentTenantIdentifierResolver; + private @Nullable CurrentTenantIdentifierResolver currentTenantIdentifierResolver; - @Nullable - private Properties hibernateProperties; + private @Nullable Properties hibernateProperties; - @Nullable - private TypeFilter[] entityTypeFilters; + private TypeFilter @Nullable [] entityTypeFilters; - @Nullable - private Class[] annotatedClasses; + private Class @Nullable [] annotatedClasses; - @Nullable - private String[] annotatedPackages; + private String @Nullable [] annotatedPackages; - @Nullable - private String[] packagesToScan; + private String @Nullable [] packagesToScan; - @Nullable - private AsyncTaskExecutor bootstrapExecutor; + private @Nullable AsyncTaskExecutor bootstrapExecutor; - @Nullable - private Integrator[] hibernateIntegrators; + private Integrator @Nullable [] hibernateIntegrators; private boolean metadataSourcesAccessed = false; - @Nullable - private MetadataSources metadataSources; + private @Nullable MetadataSources metadataSources; - @Nullable - private ResourcePatternResolver resourcePatternResolver; + private @Nullable ResourcePatternResolver resourcePatternResolver; - @Nullable - private ConfigurableListableBeanFactory beanFactory; + private @Nullable ConfigurableListableBeanFactory beanFactory; - @Nullable - private Configuration configuration; + private @Nullable Configuration configuration; - @Nullable - private SessionFactory sessionFactory; + private @Nullable SessionFactory sessionFactory; /** @@ -312,7 +286,7 @@ public void setCacheRegionFactory(RegionFactory cacheRegionFactory) { * @since 4.3 * @see LocalSessionFactoryBuilder#setMultiTenantConnectionProvider */ - public void setMultiTenantConnectionProvider(MultiTenantConnectionProvider multiTenantConnectionProvider) { + public void setMultiTenantConnectionProvider(MultiTenantConnectionProvider multiTenantConnectionProvider) { this.multiTenantConnectionProvider = multiTenantConnectionProvider; } @@ -320,7 +294,7 @@ public void setMultiTenantConnectionProvider(MultiTenantConnectionProvider multi * Set a {@link CurrentTenantIdentifierResolver} to be passed on to the SessionFactory. * @see LocalSessionFactoryBuilder#setCurrentTenantIdentifierResolver */ - public void setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { + public void setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { this.currentTenantIdentifierResolver = currentTenantIdentifierResolver; } @@ -643,8 +617,7 @@ public final Configuration getConfiguration() { @Override - @Nullable - public SessionFactory getObject() { + public @Nullable SessionFactory getObject() { return this.sessionFactory; } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java index 764e8f97d9a3..e134589b6cb6 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/LocalSessionFactoryBuilder.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. @@ -49,6 +49,7 @@ import org.hibernate.engine.jdbc.connections.spi.MultiTenantConnectionProvider; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.core.InfrastructureProxy; @@ -65,7 +66,6 @@ import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.lang.Nullable; import org.springframework.transaction.jta.JtaTransactionManager; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -120,7 +120,6 @@ public class LocalSessionFactoryBuilder extends Configuration { private final ResourcePatternResolver resourcePatternResolver; - @Nullable private TypeFilter[] entityTypeFilters = DEFAULT_ENTITY_TYPE_FILTERS; @@ -169,7 +168,7 @@ public LocalSessionFactoryBuilder( getProperties().put(AvailableSettings.CURRENT_SESSION_CONTEXT_CLASS, SpringSessionContext.class.getName()); if (dataSource != null) { - getProperties().put(AvailableSettings.DATASOURCE, dataSource); + getProperties().put(AvailableSettings.JAKARTA_NON_JTA_DATASOURCE, dataSource); } getProperties().put(AvailableSettings.CONNECTION_HANDLING, PhysicalConnectionHandlingMode.DELAYED_ACQUISITION_AND_HOLD); @@ -256,7 +255,7 @@ public LocalSessionFactoryBuilder setCacheRegionFactory(RegionFactory cacheRegio * @since 4.3 * @see AvailableSettings#MULTI_TENANT_CONNECTION_PROVIDER */ - public LocalSessionFactoryBuilder setMultiTenantConnectionProvider(MultiTenantConnectionProvider multiTenantConnectionProvider) { + public LocalSessionFactoryBuilder setMultiTenantConnectionProvider(MultiTenantConnectionProvider multiTenantConnectionProvider) { getProperties().put(AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, multiTenantConnectionProvider); return this; } @@ -267,9 +266,10 @@ public LocalSessionFactoryBuilder setMultiTenantConnectionProvider(MultiTenantCo * @see AvailableSettings#MULTI_TENANT_IDENTIFIER_RESOLVER */ @Override - public void setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { + public LocalSessionFactoryBuilder setCurrentTenantIdentifierResolver(CurrentTenantIdentifierResolver currentTenantIdentifierResolver) { getProperties().put(AvailableSettings.MULTI_TENANT_IDENTIFIER_RESOLVER, currentTenantIdentifierResolver); super.setCurrentTenantIdentifierResolver(currentTenantIdentifierResolver); + return this; } /** @@ -284,18 +284,6 @@ public LocalSessionFactoryBuilder setEntityTypeFilters(TypeFilter... entityTypeF return this; } - /** - * Add the given annotated classes in a batch. - * @see #addAnnotatedClass - * @see #scanPackages - */ - public LocalSessionFactoryBuilder addAnnotatedClasses(Class... annotatedClasses) { - for (Class annotatedClass : annotatedClasses) { - addAnnotatedClass(annotatedClass); - } - return this; - } - /** * Add the given annotated packages in a batch. * @see #addPackage diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java index 9719081da3d3..92f126a54a7c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionFactoryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,7 +16,6 @@ package org.springframework.orm.hibernate5; -import java.lang.reflect.Method; import java.util.Map; import javax.sql.DataSource; @@ -52,6 +51,7 @@ import org.hibernate.exception.LockAcquisitionException; import org.hibernate.exception.SQLGrammarException; import org.hibernate.service.UnknownServiceException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.CannotAcquireLockException; import org.springframework.dao.DataAccessException; @@ -63,9 +63,6 @@ import org.springframework.dao.InvalidDataAccessResourceUsageException; import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; /** * Helper class featuring methods for Hibernate Session handling. @@ -149,16 +146,12 @@ public static void closeSession(@Nullable Session session) { * @return the DataSource, or {@code null} if none found * @see ConnectionProvider */ - @Nullable - public static DataSource getDataSource(SessionFactory sessionFactory) { - Method getProperties = ClassUtils.getMethodIfAvailable(sessionFactory.getClass(), "getProperties"); - if (getProperties != null) { - Map props = (Map) ReflectionUtils.invokeMethod(getProperties, sessionFactory); - if (props != null) { - Object dataSourceValue = props.get(Environment.DATASOURCE); - if (dataSourceValue instanceof DataSource dataSource) { - return dataSource; - } + public static @Nullable DataSource getDataSource(SessionFactory sessionFactory) { + Map props = sessionFactory.getProperties(); + if (props != null) { + Object dataSourceValue = props.get(Environment.JAKARTA_NON_JTA_DATASOURCE); + if (dataSourceValue instanceof DataSource dataSource) { + return dataSource; } } if (sessionFactory instanceof SessionFactoryImplementor sfi) { diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java index 261035a45da7..90ea49d2a82c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SessionHolder.java @@ -19,8 +19,8 @@ import org.hibernate.FlushMode; import org.hibernate.Session; import org.hibernate.Transaction; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerHolder; /** @@ -38,11 +38,9 @@ */ public class SessionHolder extends EntityManagerHolder { - @Nullable - private Transaction transaction; + private @Nullable Transaction transaction; - @Nullable - private FlushMode previousFlushMode; + private @Nullable FlushMode previousFlushMode; public SessionHolder(Session session) { @@ -59,8 +57,7 @@ public void setTransaction(@Nullable Transaction transaction) { setTransactionActive(transaction != null); } - @Nullable - public Transaction getTransaction() { + public @Nullable Transaction getTransaction() { return this.transaction; } @@ -68,8 +65,7 @@ public void setPreviousFlushMode(@Nullable FlushMode previousFlushMode) { this.previousFlushMode = previousFlushMode; } - @Nullable - public FlushMode getPreviousFlushMode() { + public @Nullable FlushMode getPreviousFlushMode() { return this.previousFlushMode; } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java index f4c0b4149f74..f83a3f5d3dc1 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringBeanContainer.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. @@ -25,12 +25,12 @@ import org.hibernate.resource.beans.container.spi.ContainedBean; import org.hibernate.resource.beans.spi.BeanInstanceProducer; import org.hibernate.type.spi.TypeBootstrapContext; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.config.AutowireCapableBeanFactory; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ConcurrentReferenceHashMap; @@ -243,8 +243,7 @@ private static final class SpringContainedBean implements ContainedBean { private final B beanInstance; - @Nullable - private Consumer destructionCallback; + private @Nullable Consumer destructionCallback; public SpringContainedBean(B beanInstance) { this.beanInstance = beanInstance; @@ -260,6 +259,12 @@ public B getBeanInstance() { return this.beanInstance; } + @Override + @SuppressWarnings("unchecked") + public Class getBeanClass() { + return (Class) this.beanInstance.getClass(); + } + public void destroyIfNecessary() { if (this.destructionCallback != null) { this.destructionCallback.accept(this.beanInstance); diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java index 72f93fd7b64e..e444a8326fc6 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringFlushSynchronization.java @@ -17,8 +17,8 @@ package org.springframework.orm.hibernate5; import org.hibernate.Session; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronization; /** diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java index ea814641ca2a..144d3f0c8c1f 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/SpringSessionContext.java @@ -26,8 +26,8 @@ import org.hibernate.context.spi.CurrentSessionContext; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.transaction.jta.platform.spi.JtaPlatform; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -48,11 +48,9 @@ public class SpringSessionContext implements CurrentSessionContext { private final SessionFactoryImplementor sessionFactory; - @Nullable - private TransactionManager transactionManager; + private @Nullable TransactionManager transactionManager; - @Nullable - private CurrentSessionContext jtaSessionContext; + private @Nullable CurrentSessionContext jtaSessionContext; /** @@ -62,7 +60,7 @@ public class SpringSessionContext implements CurrentSessionContext { public SpringSessionContext(SessionFactoryImplementor sessionFactory) { this.sessionFactory = sessionFactory; try { - JtaPlatform jtaPlatform = sessionFactory.getServiceRegistry().getService(JtaPlatform.class); + JtaPlatform jtaPlatform = sessionFactory.getServiceRegistry().requireService(JtaPlatform.class); this.transactionManager = jtaPlatform.retrieveTransactionManager(); if (this.transactionManager != null) { this.jtaSessionContext = new SpringJtaSessionContext(sessionFactory); diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/package-info.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/package-info.java index 0ee67d0774f4..7b8f451af2ca 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/package-info.java @@ -10,9 +10,7 @@ * *

    This package supports Hibernate 5.x only. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm.hibernate5; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java index 14edcf28026f..9a5540922560 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/AsyncRequestInterceptor.java @@ -21,8 +21,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.hibernate.SessionFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.orm.hibernate5.SessionFactoryUtils; import org.springframework.orm.hibernate5.SessionHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java deleted file mode 100644 index 965334421dfd..000000000000 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/HibernateDaoSupport.java +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright 2002-2022 the original author 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.orm.hibernate5.support; - -import org.hibernate.Session; -import org.hibernate.SessionFactory; - -import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.dao.support.DaoSupport; -import org.springframework.lang.Nullable; -import org.springframework.orm.hibernate5.HibernateTemplate; -import org.springframework.util.Assert; - -/** - * Convenient superclass for Hibernate-based data access objects. - * - *

    Requires a {@link SessionFactory} to be set, providing a - * {@link org.springframework.orm.hibernate5.HibernateTemplate} based on it to - * subclasses through the {@link #getHibernateTemplate()} method. - * Can alternatively be initialized directly with a HibernateTemplate, - * in order to reuse the latter's settings such as the SessionFactory, - * exception translator, flush mode, etc. - * - *

    This class will create its own HibernateTemplate instance if a SessionFactory - * is passed in. The "allowCreate" flag on that HibernateTemplate will be "true" - * by default. A custom HibernateTemplate instance can be used through overriding - * {@link #createHibernateTemplate}. - * - *

    NOTE: Hibernate access code can also be coded in plain Hibernate style. - * Hence, for newly started projects, consider adopting the standard Hibernate - * style of coding data access objects instead, based on - * {@link SessionFactory#getCurrentSession()}. - * This HibernateTemplate primarily exists as a migration helper for Hibernate 3 - * based data access code, to benefit from bug fixes in Hibernate 5.x. - * - * @author Juergen Hoeller - * @since 4.2 - * @see #setSessionFactory - * @see #getHibernateTemplate - * @see org.springframework.orm.hibernate5.HibernateTemplate - */ -public abstract class HibernateDaoSupport extends DaoSupport { - - @Nullable - private HibernateTemplate hibernateTemplate; - - - /** - * Set the Hibernate SessionFactory to be used by this DAO. - * Will automatically create a HibernateTemplate for the given SessionFactory. - * @see #createHibernateTemplate - * @see #setHibernateTemplate - */ - public final void setSessionFactory(SessionFactory sessionFactory) { - if (this.hibernateTemplate == null || sessionFactory != this.hibernateTemplate.getSessionFactory()) { - this.hibernateTemplate = createHibernateTemplate(sessionFactory); - } - } - - /** - * Create a HibernateTemplate for the given SessionFactory. - * Only invoked if populating the DAO with a SessionFactory reference! - *

    Can be overridden in subclasses to provide a HibernateTemplate instance - * with different configuration, or a custom HibernateTemplate subclass. - * @param sessionFactory the Hibernate SessionFactory to create a HibernateTemplate for - * @return the new HibernateTemplate instance - * @see #setSessionFactory - */ - protected HibernateTemplate createHibernateTemplate(SessionFactory sessionFactory) { - return new HibernateTemplate(sessionFactory); - } - - /** - * Return the Hibernate SessionFactory used by this DAO. - */ - @Nullable - public final SessionFactory getSessionFactory() { - return (this.hibernateTemplate != null ? this.hibernateTemplate.getSessionFactory() : null); - } - - /** - * Set the HibernateTemplate for this DAO explicitly, - * as an alternative to specifying a SessionFactory. - * @see #setSessionFactory - */ - public final void setHibernateTemplate(@Nullable HibernateTemplate hibernateTemplate) { - this.hibernateTemplate = hibernateTemplate; - } - - /** - * Return the HibernateTemplate for this DAO, - * pre-initialized with the SessionFactory or set explicitly. - *

    Note: The returned HibernateTemplate is a shared instance. - * You may introspect its configuration, but not modify the configuration - * (other than from within an {@link #initDao} implementation). - * Consider creating a custom HibernateTemplate instance via - * {@code new HibernateTemplate(getSessionFactory())}, in which case - * you're allowed to customize the settings on the resulting instance. - */ - @Nullable - public final HibernateTemplate getHibernateTemplate() { - return this.hibernateTemplate; - } - - @Override - protected final void checkDaoConfig() { - if (this.hibernateTemplate == null) { - throw new IllegalArgumentException("'sessionFactory' or 'hibernateTemplate' is required"); - } - } - - - /** - * Conveniently obtain the current Hibernate Session. - * @return the Hibernate Session - * @throws DataAccessResourceFailureException if the Session couldn't be created - * @see SessionFactory#getCurrentSession() - */ - protected final Session currentSession() throws DataAccessResourceFailureException { - SessionFactory sessionFactory = getSessionFactory(); - Assert.state(sessionFactory != null, "No SessionFactory set"); - return sessionFactory.getCurrentSession(); - } - -} diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java index dade7e24c863..49a24fcaae37 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInViewInterceptor.java @@ -22,10 +22,10 @@ import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; import org.springframework.orm.hibernate5.SessionFactoryUtils; import org.springframework.orm.hibernate5.SessionHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -80,8 +80,7 @@ public class OpenSessionInViewInterceptor implements AsyncWebRequestInterceptor protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private SessionFactory sessionFactory; + private @Nullable SessionFactory sessionFactory; /** @@ -94,8 +93,7 @@ public void setSessionFactory(@Nullable SessionFactory sessionFactory) { /** * Return the Hibernate SessionFactory that should be used to create Hibernate Sessions. */ - @Nullable - public SessionFactory getSessionFactory() { + public @Nullable SessionFactory getSessionFactory() { return this.sessionFactory; } diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java index a2016e8314a1..267243c4c8cb 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/OpenSessionInterceptor.java @@ -22,10 +22,10 @@ import org.hibernate.HibernateException; import org.hibernate.Session; import org.hibernate.SessionFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; import org.springframework.orm.hibernate5.SessionFactoryUtils; import org.springframework.orm.hibernate5.SessionHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -50,8 +50,7 @@ */ public class OpenSessionInterceptor implements MethodInterceptor, InitializingBean { - @Nullable - private SessionFactory sessionFactory; + private @Nullable SessionFactory sessionFactory; /** @@ -64,8 +63,7 @@ public void setSessionFactory(@Nullable SessionFactory sessionFactory) { /** * Return the Hibernate SessionFactory that should be used to create Hibernate Sessions. */ - @Nullable - public SessionFactory getSessionFactory() { + public @Nullable SessionFactory getSessionFactory() { return this.sessionFactory; } @@ -78,8 +76,7 @@ public void afterPropertiesSet() { @Override - @Nullable - public Object invoke(MethodInvocation invocation) throws Throwable { + public @Nullable Object invoke(MethodInvocation invocation) throws Throwable { SessionFactory sf = getSessionFactory(); Assert.state(sf != null, "No SessionFactory set"); diff --git a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/package-info.java b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/package-info.java index 8eb94296292a..c6a1cbfd087a 100644 --- a/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/hibernate5/support/package-info.java @@ -1,9 +1,7 @@ /** * Classes supporting the {@code org.springframework.orm.hibernate5} package. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm.hibernate5.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java index 94c88ec65fdf..d645fa1e0f0e 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/AbstractEntityManagerFactoryBean.java @@ -45,6 +45,7 @@ import jakarta.persistence.spi.PersistenceUnitInfo; 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.BeanClassLoaderAware; @@ -58,7 +59,6 @@ import org.springframework.core.task.AsyncTaskExecutor; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -98,51 +98,38 @@ public abstract class AbstractEntityManagerFactoryBean implements /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private PersistenceProvider persistenceProvider; + private @Nullable PersistenceProvider persistenceProvider; - @Nullable - private String persistenceUnitName; + private @Nullable String persistenceUnitName; private final Map jpaPropertyMap = new HashMap<>(); - @Nullable - private Class entityManagerFactoryInterface; + private @Nullable Class entityManagerFactoryInterface; - @Nullable - private Class entityManagerInterface; + private @Nullable Class entityManagerInterface; - @Nullable - private JpaDialect jpaDialect; + private @Nullable JpaDialect jpaDialect; - @Nullable - private JpaVendorAdapter jpaVendorAdapter; + private @Nullable JpaVendorAdapter jpaVendorAdapter; - @Nullable - private Consumer entityManagerInitializer; + private @Nullable Consumer entityManagerInitializer; - @Nullable - private AsyncTaskExecutor bootstrapExecutor; + private @Nullable AsyncTaskExecutor bootstrapExecutor; private ClassLoader beanClassLoader = getClass().getClassLoader(); - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; - @Nullable - private String beanName; + private @Nullable String beanName; /** Raw EntityManagerFactory as returned by the PersistenceProvider. */ - @Nullable - private EntityManagerFactory nativeEntityManagerFactory; + private @Nullable EntityManagerFactory nativeEntityManagerFactory; /** Future for lazily initializing raw target EntityManagerFactory. */ - @Nullable - private Future nativeEntityManagerFactoryFuture; + private @Nullable Future nativeEntityManagerFactoryFuture; /** Exposed client-level EntityManagerFactory proxy. */ - @Nullable - private EntityManagerFactory entityManagerFactory; + private @Nullable EntityManagerFactory entityManagerFactory; /** @@ -172,8 +159,7 @@ public void setPersistenceProvider(@Nullable PersistenceProvider persistenceProv } @Override - @Nullable - public PersistenceProvider getPersistenceProvider() { + public @Nullable PersistenceProvider getPersistenceProvider() { return this.persistenceProvider; } @@ -189,8 +175,7 @@ public void setPersistenceUnitName(@Nullable String persistenceUnitName) { } @Override - @Nullable - public String getPersistenceUnitName() { + public @Nullable String getPersistenceUnitName() { return this.persistenceUnitName; } @@ -255,8 +240,7 @@ public void setEntityManagerInterface(@Nullable Class e } @Override - @Nullable - public Class getEntityManagerInterface() { + public @Nullable Class getEntityManagerInterface() { return this.entityManagerInterface; } @@ -272,8 +256,7 @@ public void setJpaDialect(@Nullable JpaDialect jpaDialect) { } @Override - @Nullable - public JpaDialect getJpaDialect() { + public @Nullable JpaDialect getJpaDialect() { return this.jpaDialect; } @@ -291,8 +274,7 @@ public void setJpaVendorAdapter(@Nullable JpaVendorAdapter jpaVendorAdapter) { * Return the JpaVendorAdapter implementation for this EntityManagerFactory, * or {@code null} if not known. */ - @Nullable - public JpaVendorAdapter getJpaVendorAdapter() { + public @Nullable JpaVendorAdapter getJpaVendorAdapter() { return this.jpaVendorAdapter; } @@ -332,8 +314,7 @@ public void setBootstrapExecutor(@Nullable AsyncTaskExecutor bootstrapExecutor) * Return the asynchronous executor for background bootstrapping, if any. * @since 4.3 */ - @Nullable - public AsyncTaskExecutor getBootstrapExecutor() { + public @Nullable AsyncTaskExecutor getBootstrapExecutor() { return this.bootstrapExecutor; } @@ -492,7 +473,7 @@ else if (emf != null) { * Delegate an incoming invocation from the proxy, dispatching to EntityManagerFactoryInfo * or the native EntityManagerFactory accordingly. */ - Object invokeProxyMethod(Method method, @Nullable Object[] args) throws Throwable { + Object invokeProxyMethod(Method method, Object @Nullable [] args) throws Throwable { if (method.getDeclaringClass().isAssignableFrom(EntityManagerFactoryInfo.class)) { return method.invoke(this, args); } @@ -554,8 +535,7 @@ else if (method.getName().equals("createEntityManager") && args != null && args. * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible */ @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { JpaDialect jpaDialect = getJpaDialect(); return (jpaDialect != null ? jpaDialect.translateExceptionIfPossible(ex) : EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex)); @@ -618,14 +598,12 @@ protected void postProcessEntityManager(EntityManager rawEntityManager) { } @Override - @Nullable - public PersistenceUnitInfo getPersistenceUnitInfo() { + public @Nullable PersistenceUnitInfo getPersistenceUnitInfo() { return null; } @Override - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { return null; } @@ -634,8 +612,7 @@ public DataSource getDataSource() { * Return the singleton EntityManagerFactory. */ @Override - @Nullable - public EntityManagerFactory getObject() { + public @Nullable EntityManagerFactory getObject() { return this.entityManagerFactory; } @@ -719,8 +696,7 @@ public ManagedEntityManagerFactoryInvocationHandler(AbstractEntityManagerFactory } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { switch (method.getName()) { case "equals" -> { // Only consider equal when proxies are identical. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java index f562fa03e249..0aca79d5ca31 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/DefaultJpaDialect.java @@ -21,10 +21,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.datasource.ConnectionHandle; -import org.springframework.lang.Nullable; import org.springframework.transaction.InvalidIsolationLevelException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -56,8 +56,7 @@ public class DefaultJpaDialect implements JpaDialect, Serializable { * @see #cleanupTransaction */ @Override - @Nullable - public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + public @Nullable Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { if (definition.getIsolationLevel() != TransactionDefinition.ISOLATION_DEFAULT) { @@ -70,8 +69,7 @@ public Object beginTransaction(EntityManager entityManager, TransactionDefinitio } @Override - @Nullable - public Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullable String name) + public @Nullable Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullable String name) throws PersistenceException { return null; @@ -91,8 +89,7 @@ public void cleanupTransaction(@Nullable Object transactionData) { * indicating that no JDBC Connection can be provided. */ @Override - @Nullable - public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) + public @Nullable ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) throws PersistenceException, SQLException { return null; @@ -121,8 +118,7 @@ public void releaseJdbcConnection(ConnectionHandle conHandle, EntityManager em) * @see EntityManagerFactoryUtils#convertJpaAccessExceptionIfPossible */ @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { return EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java index 32bb7fdd609c..f034fcaf2eb9 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryAccessor.java @@ -24,12 +24,12 @@ import jakarta.persistence.EntityManagerFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -46,11 +46,9 @@ public abstract class EntityManagerFactoryAccessor implements BeanFactoryAware { /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private EntityManagerFactory entityManagerFactory; + private @Nullable EntityManagerFactory entityManagerFactory; - @Nullable - private String persistenceUnitName; + private @Nullable String persistenceUnitName; private final Map jpaPropertyMap = new HashMap<>(); @@ -69,8 +67,7 @@ public void setEntityManagerFactory(@Nullable EntityManagerFactory emf) { * Return the JPA EntityManagerFactory that should be used to create * EntityManagers. */ - @Nullable - public EntityManagerFactory getEntityManagerFactory() { + public @Nullable EntityManagerFactory getEntityManagerFactory() { return this.entityManagerFactory; } @@ -101,8 +98,7 @@ public void setPersistenceUnitName(@Nullable String persistenceUnitName) { /** * Return the name of the persistence unit to access the EntityManagerFactory for, if any. */ - @Nullable - public String getPersistenceUnitName() { + public @Nullable String getPersistenceUnitName() { return this.persistenceUnitName; } @@ -176,8 +172,7 @@ protected EntityManager createEntityManager() throws IllegalStateException { * @see EntityManagerFactoryUtils#getTransactionalEntityManager(jakarta.persistence.EntityManagerFactory) * @see EntityManagerFactoryUtils#getTransactionalEntityManager(jakarta.persistence.EntityManagerFactory, java.util.Map) */ - @Nullable - protected EntityManager getTransactionalEntityManager() throws IllegalStateException{ + protected @Nullable EntityManager getTransactionalEntityManager() throws IllegalStateException{ EntityManagerFactory emf = obtainEntityManagerFactory(); return EntityManagerFactoryUtils.getTransactionalEntityManager(emf, getJpaPropertyMap()); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java index cdb9dda3ce55..8d8d649c0c9d 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryInfo.java @@ -24,8 +24,7 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceUnitInfo; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Metadata interface for a Spring-managed JPA {@link EntityManagerFactory}. @@ -46,8 +45,7 @@ public interface EntityManagerFactoryInfo { * or {@code null} if the standard JPA provider autodetection process * was used to configure the EntityManagerFactory */ - @Nullable - PersistenceProvider getPersistenceProvider(); + @Nullable PersistenceProvider getPersistenceProvider(); /** * Return the PersistenceUnitInfo used to create this @@ -56,8 +54,7 @@ public interface EntityManagerFactoryInfo { * or {@code null} if the in-container contract was not used to * configure the EntityManagerFactory */ - @Nullable - PersistenceUnitInfo getPersistenceUnitInfo(); + @Nullable PersistenceUnitInfo getPersistenceUnitInfo(); /** * Return the name of the persistence unit used to create this @@ -68,16 +65,14 @@ public interface EntityManagerFactoryInfo { * @see #getPersistenceUnitInfo() * @see jakarta.persistence.spi.PersistenceUnitInfo#getPersistenceUnitName() */ - @Nullable - String getPersistenceUnitName(); + @Nullable String getPersistenceUnitName(); /** * Return the JDBC DataSource that this EntityManagerFactory * obtains its JDBC Connections from. * @return the JDBC DataSource, or {@code null} if not known */ - @Nullable - DataSource getDataSource(); + @Nullable DataSource getDataSource(); /** * Return the (potentially vendor-specific) EntityManager interface @@ -86,15 +81,13 @@ public interface EntityManagerFactoryInfo { * to happen: either based on a target {@code EntityManager} instance * or simply defaulting to {@code jakarta.persistence.EntityManager}. */ - @Nullable - Class getEntityManagerInterface(); + @Nullable Class getEntityManagerInterface(); /** * Return the vendor-specific JpaDialect implementation for this * EntityManagerFactory, or {@code null} if not known. */ - @Nullable - JpaDialect getJpaDialect(); + @Nullable JpaDialect getJpaDialect(); /** * Return the ClassLoader that the application's beans are loaded with. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java index b9f38e313b44..9f194f526be5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerFactoryUtils.java @@ -34,6 +34,7 @@ import jakarta.persistence.TransactionRequiredException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; @@ -48,7 +49,6 @@ import org.springframework.dao.InvalidDataAccessApiUsageException; import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.jdbc.datasource.DataSourceUtils; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -130,8 +130,7 @@ public static EntityManagerFactory findEntityManagerFactory( * @throws DataAccessResourceFailureException if the EntityManager couldn't be obtained * @see JpaTransactionManager */ - @Nullable - public static EntityManager getTransactionalEntityManager(EntityManagerFactory emf) + public static @Nullable EntityManager getTransactionalEntityManager(EntityManagerFactory emf) throws DataAccessResourceFailureException { return getTransactionalEntityManager(emf, null); @@ -148,8 +147,7 @@ public static EntityManager getTransactionalEntityManager(EntityManagerFactory e * @throws DataAccessResourceFailureException if the EntityManager couldn't be obtained * @see JpaTransactionManager */ - @Nullable - public static EntityManager getTransactionalEntityManager(EntityManagerFactory emf, @Nullable Map properties) + public static @Nullable EntityManager getTransactionalEntityManager(EntityManagerFactory emf, @Nullable Map properties) throws DataAccessResourceFailureException { try { return doGetTransactionalEntityManager(emf, properties, true); @@ -171,8 +169,7 @@ public static EntityManager getTransactionalEntityManager(EntityManagerFactory e * @see #getTransactionalEntityManager(jakarta.persistence.EntityManagerFactory) * @see JpaTransactionManager */ - @Nullable - public static EntityManager doGetTransactionalEntityManager(EntityManagerFactory emf, Map properties) + public static @Nullable EntityManager doGetTransactionalEntityManager(EntityManagerFactory emf, Map properties) throws PersistenceException { return doGetTransactionalEntityManager(emf, properties, true); @@ -192,8 +189,7 @@ public static EntityManager doGetTransactionalEntityManager(EntityManagerFactory * @see #getTransactionalEntityManager(jakarta.persistence.EntityManagerFactory) * @see JpaTransactionManager */ - @Nullable - public static EntityManager doGetTransactionalEntityManager( + public static @Nullable EntityManager doGetTransactionalEntityManager( EntityManagerFactory emf, @Nullable Map properties, boolean synchronizedWithTransaction) throws PersistenceException { @@ -300,8 +296,7 @@ else if (!TransactionSynchronizationManager.isSynchronizationActive()) { * (to be passed into cleanupTransaction) * @see JpaDialect#prepareTransaction */ - @Nullable - private static Object prepareTransaction(EntityManager em, EntityManagerFactory emf) { + private static @Nullable Object prepareTransaction(EntityManager em, EntityManagerFactory emf) { if (emf instanceof EntityManagerFactoryInfo emfInfo) { JpaDialect jpaDialect = emfInfo.getJpaDialect(); if (jpaDialect != null) { @@ -360,8 +355,7 @@ public static void applyTransactionTimeout(Query query, EntityManagerFactory emf * @return the corresponding DataAccessException instance, * or {@code null} if the exception should not be translated */ - @Nullable - public static DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex) { + public static @Nullable DataAccessException convertJpaAccessExceptionIfPossible(RuntimeException ex) { // Following the JPA specification, a persistence provider can also // throw these two exceptions, besides PersistenceException. if (ex instanceof IllegalStateException) { @@ -441,11 +435,9 @@ private static class TransactionalEntityManagerSynchronization extends ResourceHolderSynchronization implements Ordered { - @Nullable - private final Object transactionData; + private final @Nullable Object transactionData; - @Nullable - private final JpaDialect jpaDialect; + private final @Nullable JpaDialect jpaDialect; private final boolean newEntityManager; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java index cc72652674dd..1c5c09df6482 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerHolder.java @@ -17,8 +17,8 @@ package org.springframework.orm.jpa; import jakarta.persistence.EntityManager; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.transaction.SavepointManager; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -40,13 +40,11 @@ */ public class EntityManagerHolder extends ResourceHolderSupport { - @Nullable - private final EntityManager entityManager; + private final @Nullable EntityManager entityManager; private boolean transactionActive; - @Nullable - private SavepointManager savepointManager; + private @Nullable SavepointManager savepointManager; public EntityManagerHolder(@Nullable EntityManager entityManager) { @@ -71,8 +69,7 @@ protected void setSavepointManager(@Nullable SavepointManager savepointManager) this.savepointManager = savepointManager; } - @Nullable - protected SavepointManager getSavepointManager() { + protected @Nullable SavepointManager getSavepointManager() { return this.savepointManager; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java index 72891dffdc02..3ed7953dc7f5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/EntityManagerRuntimeHints.java @@ -18,11 +18,12 @@ import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.ExecutableMode; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java index 6acec2dfa543..9e7716a5a13d 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/ExtendedEntityManagerCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -33,11 +33,11 @@ import jakarta.persistence.spi.PersistenceUnitTransactionType; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.Ordered; import org.springframework.dao.DataAccessException; import org.springframework.dao.support.PersistenceExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSynchronization; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.Assert; @@ -192,6 +192,7 @@ public static EntityManager createContainerManagedEntityManager( * transactions (according to the JPA 2.1 SynchronizationType rules) * @return the EntityManager proxy */ + @SuppressWarnings("removal") private static EntityManager createProxy(EntityManager rawEntityManager, EntityManagerFactoryInfo emfInfo, boolean containerManaged, boolean synchronizedWithTransaction) { @@ -260,8 +261,7 @@ private static final class ExtendedEntityManagerInvocationHandler implements Inv private final EntityManager target; - @Nullable - private final PersistenceExceptionTranslator exceptionTranslator; + private final @Nullable PersistenceExceptionTranslator exceptionTranslator; private final boolean jta; @@ -292,8 +292,7 @@ private boolean isJtaEntityManager() { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on EntityManager interface coming in... switch (method.getName()) { @@ -438,8 +437,7 @@ private static class ExtendedEntityManagerSynchronization private final EntityManager entityManager; - @Nullable - private final PersistenceExceptionTranslator exceptionTranslator; + private final @Nullable PersistenceExceptionTranslator exceptionTranslator; public volatile boolean closeOnCompletion; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java index 61eaa9bef2d5..cbccef3da2a8 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaDialect.java @@ -20,10 +20,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.PersistenceException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.support.PersistenceExceptionTranslator; import org.springframework.jdbc.datasource.ConnectionHandle; -import org.springframework.lang.Nullable; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -80,8 +80,7 @@ public interface JpaDialect extends PersistenceExceptionTranslator { * @see jakarta.persistence.EntityTransaction#begin * @see org.springframework.jdbc.datasource.DataSourceUtils#prepareConnectionForTransaction */ - @Nullable - Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + @Nullable Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException; /** @@ -103,8 +102,7 @@ Object beginTransaction(EntityManager entityManager, TransactionDefinition defin * @throws jakarta.persistence.PersistenceException if thrown by JPA methods * @see #cleanupTransaction */ - @Nullable - Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullable String name) + @Nullable Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullable String name) throws PersistenceException; /** @@ -150,8 +148,7 @@ Object prepareTransaction(EntityManager entityManager, boolean readOnly, @Nullab * @see org.springframework.jdbc.datasource.SimpleConnectionHandle * @see JpaTransactionManager#setDataSource */ - @Nullable - ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) + @Nullable ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean readOnly) throws PersistenceException, SQLException; /** diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java index 168938c70b72..9b8d020f877b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaTransactionManager.java @@ -28,6 +28,7 @@ import jakarta.persistence.EntityTransaction; import jakarta.persistence.PersistenceException; import jakarta.persistence.RollbackException; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; @@ -40,7 +41,6 @@ import org.springframework.jdbc.datasource.ConnectionHolder; import org.springframework.jdbc.datasource.JdbcTransactionObjectSupport; import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy; -import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.IllegalTransactionStateException; import org.springframework.transaction.NestedTransactionNotSupportedException; @@ -117,21 +117,17 @@ public class JpaTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, BeanFactoryAware, InitializingBean { - @Nullable - private EntityManagerFactory entityManagerFactory; + private @Nullable EntityManagerFactory entityManagerFactory; - @Nullable - private String persistenceUnitName; + private @Nullable String persistenceUnitName; private final Map jpaPropertyMap = new HashMap<>(); - @Nullable - private DataSource dataSource; + private @Nullable DataSource dataSource; private JpaDialect jpaDialect = new DefaultJpaDialect(); - @Nullable - private Consumer entityManagerInitializer; + private @Nullable Consumer entityManagerInitializer; /** @@ -168,8 +164,7 @@ public void setEntityManagerFactory(@Nullable EntityManagerFactory emf) { /** * Return the EntityManagerFactory that this instance should manage transactions for. */ - @Nullable - public EntityManagerFactory getEntityManagerFactory() { + public @Nullable EntityManagerFactory getEntityManagerFactory() { return this.entityManagerFactory; } @@ -200,8 +195,7 @@ public void setPersistenceUnitName(@Nullable String persistenceUnitName) { /** * Return the name of the persistence unit to manage transactions for, if any. */ - @Nullable - public String getPersistenceUnitName() { + public @Nullable String getPersistenceUnitName() { return this.persistenceUnitName; } @@ -276,8 +270,7 @@ public void setDataSource(@Nullable DataSource dataSource) { /** * Return the JDBC DataSource that this instance manages transactions for. */ - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { return this.dataSource; } @@ -665,13 +658,11 @@ protected void doCleanupAfterCompletion(Object transaction) { */ private class JpaTransactionObject extends JdbcTransactionObjectSupport { - @Nullable - private EntityManagerHolder entityManagerHolder; + private @Nullable EntityManagerHolder entityManagerHolder; private boolean newEntityManagerHolder; - @Nullable - private Object transactionData; + private @Nullable Object transactionData; public void setEntityManagerHolder( @Nullable EntityManagerHolder entityManagerHolder, boolean newEntityManagerHolder) { @@ -705,8 +696,7 @@ public void setTransactionData(@Nullable Object transactionData) { } } - @Nullable - public Object getTransactionData() { + public @Nullable Object getTransactionData() { return this.transactionData; } @@ -808,8 +798,7 @@ private static final class SuspendedResourcesHolder { private final EntityManagerHolder entityManagerHolder; - @Nullable - private final ConnectionHolder connectionHolder; + private final @Nullable ConnectionHolder connectionHolder; private SuspendedResourcesHolder(EntityManagerHolder emHolder, @Nullable ConnectionHolder conHolder) { this.entityManagerHolder = emHolder; @@ -820,8 +809,7 @@ private EntityManagerHolder getEntityManagerHolder() { return this.entityManagerHolder; } - @Nullable - private ConnectionHolder getConnectionHolder() { + private @Nullable ConnectionHolder getConnectionHolder() { return this.connectionHolder; } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java index d55a6b614b49..1120ad11379f 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/JpaVendorAdapter.java @@ -23,8 +23,7 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceUnitInfo; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * SPI interface that allows to plug in vendor-specific behavior @@ -49,8 +48,7 @@ public interface JpaVendorAdapter { * excluding provider classes from temporary class overriding. * @since 2.5.2 */ - @Nullable - default String getPersistenceProviderRootPackage() { + default @Nullable String getPersistenceProviderRootPackage() { return null; } @@ -99,8 +97,7 @@ default String getPersistenceProviderRootPackage() { * Return the vendor-specific JpaDialect implementation for this * provider, or {@code null} if there is none. */ - @Nullable - default JpaDialect getJpaDialect() { + default @Nullable JpaDialect getJpaDialect() { return null; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java index bc66db3acf11..ad9e00a4b31b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBean.java @@ -24,6 +24,7 @@ import jakarta.persistence.ValidationMode; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceUnitInfo; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.context.ResourceLoaderAware; @@ -31,7 +32,6 @@ import org.springframework.core.io.ResourceLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.jdbc.datasource.lookup.SingleDataSourceLookup; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.persistenceunit.DefaultPersistenceUnitManager; import org.springframework.orm.jpa.persistenceunit.ManagedClassNameFilter; import org.springframework.orm.jpa.persistenceunit.PersistenceManagedTypes; @@ -89,13 +89,11 @@ public class LocalContainerEntityManagerFactoryBean extends AbstractEntityManagerFactoryBean implements ResourceLoaderAware, LoadTimeWeaverAware { - @Nullable - private PersistenceUnitManager persistenceUnitManager; + private @Nullable PersistenceUnitManager persistenceUnitManager; private final DefaultPersistenceUnitManager internalPersistenceUnitManager = new DefaultPersistenceUnitManager(); - @Nullable - private PersistenceUnitInfo persistenceUnitInfo; + private @Nullable PersistenceUnitInfo persistenceUnitInfo; /** @@ -426,14 +424,12 @@ protected void postProcessEntityManagerFactory(EntityManagerFactory emf, Persist @Override - @Nullable - public PersistenceUnitInfo getPersistenceUnitInfo() { + public @Nullable PersistenceUnitInfo getPersistenceUnitInfo() { return this.persistenceUnitInfo; } @Override - @Nullable - public String getPersistenceUnitName() { + public @Nullable String getPersistenceUnitName() { if (this.persistenceUnitInfo != null) { return this.persistenceUnitInfo.getPersistenceUnitName(); } @@ -441,8 +437,7 @@ public String getPersistenceUnitName() { } @Override - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { if (this.persistenceUnitInfo != null) { return (this.persistenceUnitInfo.getJtaDataSource() != null ? this.persistenceUnitInfo.getJtaDataSource() : diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java index 88b3d1128fa3..31e204c21dc5 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBean.java @@ -22,8 +22,7 @@ import jakarta.persistence.Persistence; import jakarta.persistence.PersistenceException; import jakarta.persistence.spi.PersistenceProvider; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link org.springframework.beans.factory.FactoryBean} that creates a JPA @@ -91,8 +90,7 @@ public void setDataSource(@Nullable DataSource dataSource) { * @see #getJpaPropertyMap() */ @Override - @Nullable - public DataSource getDataSource() { + public @Nullable DataSource getDataSource() { return (DataSource) getJpaPropertyMap().get(DATASOURCE_PROPERTY); } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java index 9dfb8c7d0eb4..d3ae81bc32c2 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/SharedEntityManagerCreator.java @@ -35,8 +35,8 @@ import jakarta.persistence.TransactionRequiredException; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.TransactionSynchronizationManager; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -190,13 +190,11 @@ private static class SharedEntityManagerInvocationHandler implements InvocationH private final EntityManagerFactory targetFactory; - @Nullable - private final Map properties; + private final @Nullable Map properties; private final boolean synchronizedWithTransaction; - @Nullable - private transient volatile ClassLoader proxyClassLoader; + private transient volatile @Nullable ClassLoader proxyClassLoader; public SharedEntityManagerInvocationHandler( EntityManagerFactory target, @Nullable Map properties, boolean synchronizedWithTransaction) { @@ -217,8 +215,7 @@ private void initProxyClassLoader() { } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // Invocation on EntityManager interface coming in... switch (method.getName()) { @@ -362,11 +359,9 @@ private static class DeferredQueryInvocationHandler implements InvocationHandler private final Query target; - @Nullable - private EntityManager entityManager; + private @Nullable EntityManager entityManager; - @Nullable - private Map outputParameters; + private @Nullable Map outputParameters; public DeferredQueryInvocationHandler(Query target, EntityManager entityManager) { this.target = target; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/package-info.java b/spring-orm/src/main/java/org/springframework/orm/jpa/package-info.java index ae73e43ba2c9..1bb8358803d2 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/package-info.java @@ -3,9 +3,7 @@ * Contains EntityManagerFactory helper classes, a template plus callback for JPA access, * and an implementation of Spring's transaction SPI for local JPA transactions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm.jpa; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java index 0654e12c3d9e..529504237a00 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/ClassFileTransformerAdapter.java @@ -22,8 +22,8 @@ import jakarta.persistence.spi.ClassTransformer; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -53,8 +53,7 @@ public ClassFileTransformerAdapter(ClassTransformer classTransformer) { @Override - @Nullable - public byte[] transform( + public byte @Nullable [] transform( ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java index 058f5e9184d3..821d55da50b0 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/DefaultPersistenceUnitManager.java @@ -33,6 +33,7 @@ import jakarta.persistence.spi.PersistenceUnitInfo; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ResourceLoaderAware; @@ -47,7 +48,6 @@ import org.springframework.jdbc.datasource.lookup.DataSourceLookup; import org.springframework.jdbc.datasource.lookup.JndiDataSourceLookup; import org.springframework.jdbc.datasource.lookup.MapDataSourceLookup; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; import org.springframework.util.ResourceUtils; @@ -104,43 +104,31 @@ public class DefaultPersistenceUnitManager private String[] persistenceXmlLocations = new String[] {DEFAULT_PERSISTENCE_XML_LOCATION}; - @Nullable - private String defaultPersistenceUnitRootLocation = ORIGINAL_DEFAULT_PERSISTENCE_UNIT_ROOT_LOCATION; + private @Nullable String defaultPersistenceUnitRootLocation = ORIGINAL_DEFAULT_PERSISTENCE_UNIT_ROOT_LOCATION; - @Nullable - private String defaultPersistenceUnitName = ORIGINAL_DEFAULT_PERSISTENCE_UNIT_NAME; + private @Nullable String defaultPersistenceUnitName = ORIGINAL_DEFAULT_PERSISTENCE_UNIT_NAME; - @Nullable - private PersistenceManagedTypes managedTypes; + private @Nullable PersistenceManagedTypes managedTypes; - @Nullable - private String[] packagesToScan; + private String @Nullable [] packagesToScan; - @Nullable - private ManagedClassNameFilter managedClassNameFilter; + private @Nullable ManagedClassNameFilter managedClassNameFilter; - @Nullable - private String[] mappingResources; + private String @Nullable [] mappingResources; - @Nullable - private SharedCacheMode sharedCacheMode; + private @Nullable SharedCacheMode sharedCacheMode; - @Nullable - private ValidationMode validationMode; + private @Nullable ValidationMode validationMode; private DataSourceLookup dataSourceLookup = new JndiDataSourceLookup(); - @Nullable - private DataSource defaultDataSource; + private @Nullable DataSource defaultDataSource; - @Nullable - private DataSource defaultJtaDataSource; + private @Nullable DataSource defaultJtaDataSource; - @Nullable - private PersistenceUnitPostProcessor[] persistenceUnitPostProcessors; + private PersistenceUnitPostProcessor @Nullable [] persistenceUnitPostProcessors; - @Nullable - private LoadTimeWeaver loadTimeWeaver; + private @Nullable LoadTimeWeaver loadTimeWeaver; private ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver(); @@ -329,8 +317,7 @@ public void setDataSourceLookup(@Nullable DataSourceLookup dataSourceLookup) { * persistence provider, resolving data source names in {@code persistence.xml} * against Spring-managed DataSource instances. */ - @Nullable - public DataSourceLookup getDataSourceLookup() { + public @Nullable DataSourceLookup getDataSourceLookup() { return this.dataSourceLookup; } @@ -351,8 +338,7 @@ public void setDefaultDataSource(@Nullable DataSource defaultDataSource) { * Return the JDBC DataSource that the JPA persistence provider is supposed to use * for accessing the database if none has been specified in {@code persistence.xml}. */ - @Nullable - public DataSource getDefaultDataSource() { + public @Nullable DataSource getDefaultDataSource() { return this.defaultDataSource; } @@ -373,8 +359,7 @@ public void setDefaultJtaDataSource(@Nullable DataSource defaultJtaDataSource) { * Return the JTA-aware DataSource that the JPA persistence provider is supposed to use * for accessing the database if none has been specified in {@code persistence.xml}. */ - @Nullable - public DataSource getDefaultJtaDataSource() { + public @Nullable DataSource getDefaultJtaDataSource() { return this.defaultJtaDataSource; } @@ -384,7 +369,7 @@ public DataSource getDefaultJtaDataSource() { *

    Such post-processors can, for example, register further entity classes and * jar files, in addition to the metadata read from {@code persistence.xml}. */ - public void setPersistenceUnitPostProcessors(@Nullable PersistenceUnitPostProcessor... postProcessors) { + public void setPersistenceUnitPostProcessors(PersistenceUnitPostProcessor @Nullable ... postProcessors) { this.persistenceUnitPostProcessors = postProcessors; } @@ -392,8 +377,7 @@ public void setPersistenceUnitPostProcessors(@Nullable PersistenceUnitPostProces * Return the PersistenceUnitPostProcessors to be applied to each * PersistenceUnitInfo that has been parsed by this manager. */ - @Nullable - public PersistenceUnitPostProcessor[] getPersistenceUnitPostProcessors() { + public PersistenceUnitPostProcessor @Nullable [] getPersistenceUnitPostProcessors() { return this.persistenceUnitPostProcessors; } @@ -424,8 +408,7 @@ public void setLoadTimeWeaver(@Nullable LoadTimeWeaver loadTimeWeaver) { * Return the Spring LoadTimeWeaver to use for class instrumentation according * to the JPA class transformer contract. */ - @Nullable - public LoadTimeWeaver getLoadTimeWeaver() { + public @Nullable LoadTimeWeaver getLoadTimeWeaver() { return this.loadTimeWeaver; } @@ -452,7 +435,7 @@ public void afterPropertiesSet() { * @see #obtainDefaultPersistenceUnitInfo() * @see #obtainPersistenceUnitInfo(String) */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void preparePersistenceUnitInfos() { this.persistenceUnitInfoNames.clear(); this.persistenceUnitInfos.clear(); @@ -594,8 +577,7 @@ private void applyManagedTypes(SpringPersistenceUnitInfo scannedUnit, Persistenc * @return the persistence unit root URL to pass to the JPA PersistenceProvider * @see #setDefaultPersistenceUnitRootLocation */ - @Nullable - private URL determineDefaultPersistenceUnitRootUrl() { + private @Nullable URL determineDefaultPersistenceUnitRootUrl() { if (this.defaultPersistenceUnitRootLocation == null) { return null; } @@ -618,8 +600,7 @@ private URL determineDefaultPersistenceUnitRootUrl() { *

    Checks whether a "META-INF/orm.xml" file exists in the classpath and uses it * if it is not co-located with a "META-INF/persistence.xml" file. */ - @Nullable - private Resource getOrmXmlForDefaultPersistenceUnit() { + private @Nullable Resource getOrmXmlForDefaultPersistenceUnit() { Resource ormXml = this.resourcePatternResolver.getResource( this.defaultPersistenceUnitRootLocation + DEFAULT_ORM_XML_RESOURCE); if (ormXml.exists()) { @@ -647,8 +628,7 @@ private Resource getOrmXmlForDefaultPersistenceUnit() { * @param persistenceUnitName the name of the desired persistence unit * @return the PersistenceUnitInfo in mutable form, or {@code null} if not available */ - @Nullable - protected final MutablePersistenceUnitInfo getPersistenceUnitInfo(String persistenceUnitName) { + protected final @Nullable MutablePersistenceUnitInfo getPersistenceUnitInfo(String persistenceUnitName) { PersistenceUnitInfo pui = this.persistenceUnitInfos.get(persistenceUnitName); return (MutablePersistenceUnitInfo) pui; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java index 7bd5304b0062..c8691065a19b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/MutablePersistenceUnitInfo.java @@ -27,8 +27,8 @@ import jakarta.persistence.ValidationMode; import jakarta.persistence.spi.ClassTransformer; import jakarta.persistence.spi.PersistenceUnitTransactionType; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -45,29 +45,24 @@ * @author Costin Leau * @since 2.0 */ +@SuppressWarnings("removal") public class MutablePersistenceUnitInfo implements SmartPersistenceUnitInfo { - @Nullable - private String persistenceUnitName; + private @Nullable String persistenceUnitName; - @Nullable - private String persistenceProviderClassName; + private @Nullable String persistenceProviderClassName; - @Nullable - private PersistenceUnitTransactionType transactionType; + private @Nullable PersistenceUnitTransactionType transactionType; - @Nullable - private DataSource nonJtaDataSource; + private @Nullable DataSource nonJtaDataSource; - @Nullable - private DataSource jtaDataSource; + private @Nullable DataSource jtaDataSource; private final List mappingFileNames = new ArrayList<>(); private final List jarFileUrls = new ArrayList<>(); - @Nullable - private URL persistenceUnitRootUrl; + private @Nullable URL persistenceUnitRootUrl; private final List managedClassNames = new ArrayList<>(); @@ -83,8 +78,7 @@ public class MutablePersistenceUnitInfo implements SmartPersistenceUnitInfo { private String persistenceXMLSchemaVersion = "2.0"; - @Nullable - private String persistenceProviderPackageName; + private @Nullable String persistenceProviderPackageName; public void setPersistenceUnitName(@Nullable String persistenceUnitName) { @@ -92,8 +86,7 @@ public void setPersistenceUnitName(@Nullable String persistenceUnitName) { } @Override - @Nullable - public String getPersistenceUnitName() { + public @Nullable String getPersistenceUnitName() { return this.persistenceUnitName; } @@ -102,8 +95,7 @@ public void setPersistenceProviderClassName(@Nullable String persistenceProvider } @Override - @Nullable - public String getPersistenceProviderClassName() { + public @Nullable String getPersistenceProviderClassName() { return this.persistenceProviderClassName; } @@ -127,8 +119,7 @@ public void setJtaDataSource(@Nullable DataSource jtaDataSource) { } @Override - @Nullable - public DataSource getJtaDataSource() { + public @Nullable DataSource getJtaDataSource() { return this.jtaDataSource; } @@ -137,8 +128,7 @@ public void setNonJtaDataSource(@Nullable DataSource nonJtaDataSource) { } @Override - @Nullable - public DataSource getNonJtaDataSource() { + public @Nullable DataSource getNonJtaDataSource() { return this.nonJtaDataSource; } @@ -165,8 +155,7 @@ public void setPersistenceUnitRootUrl(@Nullable URL persistenceUnitRootUrl) { } @Override - @Nullable - public URL getPersistenceUnitRootUrl() { + public @Nullable URL getPersistenceUnitRootUrl() { return this.persistenceUnitRootUrl; } @@ -257,8 +246,7 @@ public void setPersistenceProviderPackageName(@Nullable String persistenceProvid this.persistenceProviderPackageName = persistenceProviderPackageName; } - @Nullable - public String getPersistenceProviderPackageName() { + public @Nullable String getPersistenceProviderPackageName() { return this.persistenceProviderPackageName; } @@ -268,8 +256,7 @@ public String getPersistenceProviderPackageName() { * @see org.springframework.util.ClassUtils#getDefaultClassLoader() */ @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return ClassUtils.getDefaultClassLoader(); } @@ -289,6 +276,16 @@ public ClassLoader getNewTempClassLoader() { throw new UnsupportedOperationException("getNewTempClassLoader not supported"); } + @Override + public @Nullable String getScopeAnnotationName() { + return null; + } + + @Override + public @Nullable List getQualifierAnnotationNames() { + return null; + } + @Override public String toString() { diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java index 738b14294fa0..4c953dcef452 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypes.java @@ -20,8 +20,8 @@ import java.util.List; import jakarta.persistence.spi.PersistenceUnitInfo; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -54,8 +54,7 @@ public interface PersistenceManagedTypes { * @return the persistence unit root url * @see PersistenceUnitInfo#getPersistenceUnitRootUrl() */ - @Nullable - URL getPersistenceUnitRootUrl(); + @Nullable URL getPersistenceUnitRootUrl(); /** * Create an instance using the specified managed class names. diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java index 5da0ac9866e8..6c42cba4c842 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessor.java @@ -21,6 +21,7 @@ import javax.lang.model.element.Modifier; +import jakarta.persistence.AttributeConverter; import jakarta.persistence.Convert; import jakarta.persistence.Converter; import jakarta.persistence.EntityListeners; @@ -32,6 +33,7 @@ import jakarta.persistence.PrePersist; import jakarta.persistence.PreRemove; import jakarta.persistence.PreUpdate; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; @@ -49,7 +51,6 @@ import org.springframework.core.annotation.AnnotationUtils; import org.springframework.javapoet.CodeBlock; import org.springframework.javapoet.ParameterizedTypeName; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ReflectionUtils; @@ -70,9 +71,8 @@ class PersistenceManagedTypesBeanRegistrationAotProcessor implements BeanRegistr private static final boolean jpaPresent = ClassUtils.isPresent("jakarta.persistence.Entity", PersistenceManagedTypesBeanRegistrationAotProcessor.class.getClassLoader()); - @Nullable @Override - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { if (jpaPresent) { if (PersistenceManagedTypes.class.isAssignableFrom(registeredBean.getBeanClass())) { return BeanRegistrationAotContribution.withCustomCodeFragments(codeFragments -> @@ -173,7 +173,7 @@ private void contributeConverterHints(RuntimeHints hints, Class managedClass) } ReflectionUtils.doWithFields(managedClass, field -> { Convert convertFieldAnnotation = AnnotationUtils.findAnnotation(field, Convert.class); - if (convertFieldAnnotation != null && convertFieldAnnotation.converter() != void.class) { + if (convertFieldAnnotation != null && convertFieldAnnotation.converter() != AttributeConverter.class) { reflectionHints.registerType(convertFieldAnnotation.converter(), MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } }); @@ -229,8 +229,7 @@ private void contributeHibernateHints(RuntimeHints hints, @Nullable ClassLoader } } - @Nullable - private static Class loadClass(String className, @Nullable ClassLoader classLoader) { + private static @Nullable Class loadClass(String className, @Nullable ClassLoader classLoader) { try { return (Class) ClassUtils.forName(className, classLoader); } @@ -239,13 +238,13 @@ private static Class loadClass(String className, @Nullable } } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Not null assertion performed in ReflectionHints.registerType private void registerForReflection(ReflectionHints reflection, @Nullable Annotation annotation, String attribute) { if (annotation == null) { return; } - Class embeddableInstantiatorClass = (Class) AnnotationUtils.getAnnotationAttributes(annotation).get(attribute); - reflection.registerType(embeddableInstantiatorClass, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); + Class type = (Class) AnnotationUtils.getAnnotationAttributes(annotation).get(attribute); + reflection.registerType(type, MemberCategory.INVOKE_DECLARED_CONSTRUCTORS); } } } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java index a76b1a4b588c..fef854dee334 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesScanner.java @@ -29,6 +29,7 @@ import jakarta.persistence.Entity; import jakarta.persistence.MappedSuperclass; import jakarta.persistence.PersistenceException; +import org.jspecify.annotations.Nullable; import org.springframework.context.index.CandidateComponentsIndex; import org.springframework.context.index.CandidateComponentsIndexLoader; @@ -43,7 +44,6 @@ import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; import org.springframework.util.ResourceUtils; @@ -77,8 +77,7 @@ public final class PersistenceManagedTypesScanner { private final ResourcePatternResolver resourcePatternResolver; - @Nullable - private final CandidateComponentsIndex componentsIndex; + private final @Nullable CandidateComponentsIndex componentsIndex; private final ManagedClassNameFilter managedClassNameFilter; @@ -192,8 +191,7 @@ private static class ScanResult { private final List managedPackages = new ArrayList<>(); - @Nullable - private URL persistenceUnitRootUrl; + private @Nullable URL persistenceUnitRootUrl; PersistenceManagedTypes toJpaManagedTypes() { return new SimplePersistenceManagedTypes(this.managedClassNames, diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java index 85fd8d7823fd..069420cab048 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/PersistenceUnitReader.java @@ -31,6 +31,7 @@ import jakarta.persistence.spi.PersistenceUnitTransactionType; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.xml.sax.ErrorHandler; @@ -39,7 +40,6 @@ import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.jdbc.datasource.lookup.DataSourceLookup; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ResourceUtils; import org.springframework.util.StringUtils; @@ -185,6 +185,7 @@ void parseDocument(Resource resource, Document document, List managedPackages; - @Nullable - private final URL persistenceUnitRootUrl; + private final @Nullable URL persistenceUnitRootUrl; SimplePersistenceManagedTypes(List managedClassNames, List managedPackages, @@ -60,8 +59,7 @@ public List getManagedPackages() { } @Override - @Nullable - public URL getPersistenceUnitRootUrl() { + public @Nullable URL getPersistenceUnitRootUrl() { return this.persistenceUnitRootUrl; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java index e188d4c4ce99..3dabc9807525 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/SpringPersistenceUnitInfo.java @@ -18,11 +18,11 @@ import jakarta.persistence.spi.ClassTransformer; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.DecoratingClassLoader; import org.springframework.instrument.classloading.LoadTimeWeaver; import org.springframework.instrument.classloading.SimpleThrowawayClassLoader; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -39,11 +39,9 @@ */ class SpringPersistenceUnitInfo extends MutablePersistenceUnitInfo { - @Nullable - private LoadTimeWeaver loadTimeWeaver; + private @Nullable LoadTimeWeaver loadTimeWeaver; - @Nullable - private ClassLoader classLoader; + private @Nullable ClassLoader classLoader; /** @@ -70,8 +68,7 @@ public void init(@Nullable ClassLoader classLoader) { * if specified. */ @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return this.classLoader; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package-info.java b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package-info.java index e27e279a1c13..d3f6e6a54318 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/persistenceunit/package-info.java @@ -1,9 +1,7 @@ /** * Internal support for managing JPA persistence units. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm.jpa.persistenceunit; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/AsyncRequestInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/AsyncRequestInterceptor.java index 3bee08ed148e..db6be53668cb 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/AsyncRequestInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/AsyncRequestInterceptor.java @@ -21,8 +21,8 @@ import jakarta.persistence.EntityManagerFactory; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java index 4b1854005440..a3ace5665a21 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewFilter.java @@ -25,9 +25,9 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.EntityManagerHolder; import org.springframework.transaction.support.TransactionSynchronizationManager; @@ -75,14 +75,11 @@ public class OpenEntityManagerInViewFilter extends OncePerRequestFilter { public static final String DEFAULT_ENTITY_MANAGER_FACTORY_BEAN_NAME = "entityManagerFactory"; - @Nullable - private String entityManagerFactoryBeanName; + private @Nullable String entityManagerFactoryBeanName; - @Nullable - private String persistenceUnitName; + private @Nullable String persistenceUnitName; - @Nullable - private volatile EntityManagerFactory entityManagerFactory; + private volatile @Nullable EntityManagerFactory entityManagerFactory; /** @@ -101,8 +98,7 @@ public void setEntityManagerFactoryBeanName(@Nullable String entityManagerFactor * Return the bean name of the EntityManagerFactory to fetch from Spring's * root application context. */ - @Nullable - protected String getEntityManagerFactoryBeanName() { + protected @Nullable String getEntityManagerFactoryBeanName() { return this.entityManagerFactoryBeanName; } @@ -123,8 +119,7 @@ public void setPersistenceUnitName(@Nullable String persistenceUnitName) { /** * Return the name of the persistence unit to access the EntityManagerFactory for, if any. */ - @Nullable - protected String getPersistenceUnitName() { + protected @Nullable String getPersistenceUnitName() { return this.persistenceUnitName; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java index e6cc8f14fe55..9d1f4349a441 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/OpenEntityManagerInViewInterceptor.java @@ -19,10 +19,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.PersistenceException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.DataAccessException; import org.springframework.dao.DataAccessResourceFailureException; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerFactoryAccessor; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.EntityManagerHolder; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java index f5e17433c649..b653aa1c7cfc 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessor.java @@ -38,6 +38,7 @@ import jakarta.persistence.PersistenceProperty; import jakarta.persistence.PersistenceUnit; import jakarta.persistence.SynchronizationType; +import org.jspecify.annotations.Nullable; import org.springframework.aot.generate.GeneratedClass; import org.springframework.aot.generate.GeneratedMethod; @@ -73,7 +74,6 @@ import org.springframework.javapoet.MethodSpec; import org.springframework.jndi.JndiLocatorDelegate; import org.springframework.jndi.JndiTemplate; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerFactoryInfo; import org.springframework.orm.jpa.EntityManagerFactoryUtils; import org.springframework.orm.jpa.EntityManagerProxy; @@ -191,26 +191,21 @@ public class PersistenceAnnotationBeanPostProcessor implements InstantiationAwar DestructionAwareBeanPostProcessor, MergedBeanDefinitionPostProcessor, BeanRegistrationAotProcessor, PriorityOrdered, BeanFactoryAware, Serializable { - @Nullable - private Object jndiEnvironment; + private @Nullable Object jndiEnvironment; private boolean resourceRef = true; - @Nullable - private transient Map persistenceUnits; + private transient @Nullable Map persistenceUnits; - @Nullable - private transient Map persistenceContexts; + private transient @Nullable Map persistenceContexts; - @Nullable - private transient Map extendedPersistenceContexts; + private transient @Nullable Map extendedPersistenceContexts; private transient String defaultPersistenceUnitName = ""; private int order = Ordered.LOWEST_PRECEDENCE - 4; - @Nullable - private transient ListableBeanFactory beanFactory; + private transient @Nullable ListableBeanFactory beanFactory; private final transient Map injectionMetadataCache = new ConcurrentHashMap<>(256); @@ -359,8 +354,7 @@ public void resetBeanDefinition(String beanName) { } @Override - @Nullable - public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + public @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { Class beanClass = registeredBean.getBeanClass(); String beanName = registeredBean.getBeanName(); RootBeanDefinition beanDefinition = registeredBean.getMergedBeanDefinition(); @@ -479,8 +473,7 @@ private InjectionMetadata buildPersistenceMetadata(Class clazz) { * or {@code null} if none found * @see #setPersistenceUnits */ - @Nullable - protected EntityManagerFactory getPersistenceUnit(@Nullable String unitName) { + protected @Nullable EntityManagerFactory getPersistenceUnit(@Nullable String unitName) { if (this.persistenceUnits != null) { String unitNameForLookup = (unitName != null ? unitName : ""); if (unitNameForLookup.isEmpty()) { @@ -511,8 +504,7 @@ protected EntityManagerFactory getPersistenceUnit(@Nullable String unitName) { * @see #setPersistenceContexts * @see #setExtendedPersistenceContexts */ - @Nullable - protected EntityManager getPersistenceContext(@Nullable String unitName, boolean extended) { + protected @Nullable EntityManager getPersistenceContext(@Nullable String unitName, boolean extended) { Map contexts = (extended ? this.extendedPersistenceContexts : this.persistenceContexts); if (contexts != null) { String unitNameForLookup = (unitName != null ? unitName : ""); @@ -648,13 +640,11 @@ private class PersistenceElement extends InjectionMetadata.InjectedElement { private final String unitName; - @Nullable - private PersistenceContextType type; + private @Nullable PersistenceContextType type; private boolean synchronizedWithTransaction = false; - @Nullable - private Properties properties; + private @Nullable Properties properties; public PersistenceElement(Member member, AnnotatedElement ae, @Nullable PropertyDescriptor pd) { super(member, pd); @@ -858,7 +848,7 @@ private CodeBlock generateResourceToInjectCode( return CodeBlock.of("$L($L)", generatedMethod.getName(), REGISTERED_BEAN_PARAMETER); } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private void generateGetEntityManagerMethod(MethodSpec.Builder method, PersistenceElement injectedElement) { String unitName = injectedElement.unitName; Properties properties = injectedElement.properties; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java index 0b460b000014..61b7a7d0de30 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/SharedEntityManagerBean.java @@ -18,10 +18,10 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.EntityManagerFactoryAccessor; import org.springframework.orm.jpa.EntityManagerFactoryInfo; import org.springframework.orm.jpa.SharedEntityManagerCreator; @@ -52,13 +52,11 @@ public class SharedEntityManagerBean extends EntityManagerFactoryAccessor implements FactoryBean, InitializingBean { - @Nullable - private Class entityManagerInterface; + private @Nullable Class entityManagerInterface; private boolean synchronizedWithTransaction = true; - @Nullable - private EntityManager shared; + private @Nullable EntityManager shared; /** @@ -108,8 +106,7 @@ public final void afterPropertiesSet() { @Override - @Nullable - public EntityManager getObject() { + public @Nullable EntityManager getObject() { return this.shared; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/support/package-info.java b/spring-orm/src/main/java/org/springframework/orm/jpa/support/package-info.java index b2b1b32ba47c..f8486431655b 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/support/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/support/package-info.java @@ -1,9 +1,7 @@ /** * Classes supporting the {@code org.springframework.orm.jpa} package. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm.jpa.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java index f79bb3f5d858..88713bbed664 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/AbstractJpaVendorAdapter.java @@ -22,8 +22,8 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.spi.PersistenceUnitInfo; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.JpaDialect; import org.springframework.orm.jpa.JpaVendorAdapter; @@ -39,8 +39,7 @@ public abstract class AbstractJpaVendorAdapter implements JpaVendorAdapter { private Database database = Database.DEFAULT; - @Nullable - private String databasePlatform; + private @Nullable String databasePlatform; private boolean generateDdl = false; @@ -77,8 +76,7 @@ public void setDatabasePlatform(@Nullable String databasePlatform) { /** * Return the name of the target database to operate on. */ - @Nullable - protected String getDatabasePlatform() { + protected @Nullable String getDatabasePlatform() { return this.databasePlatform; } @@ -125,8 +123,7 @@ protected boolean isShowSql() { @Override - @Nullable - public String getPersistenceProviderRootPackage() { + public @Nullable String getPersistenceProviderRootPackage() { return null; } @@ -141,8 +138,7 @@ public String getPersistenceProviderRootPackage() { } @Override - @Nullable - public JpaDialect getJpaDialect() { + public @Nullable JpaDialect getJpaDialect() { return null; } diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java index 5b313816d120..b8f2ee89d541 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaDialect.java @@ -25,9 +25,9 @@ import jakarta.persistence.PersistenceException; import org.eclipse.persistence.sessions.DatabaseLogin; import org.eclipse.persistence.sessions.UnitOfWork; +import org.jspecify.annotations.Nullable; import org.springframework.jdbc.datasource.ConnectionHandle; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.DefaultJpaDialect; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.TransactionException; @@ -96,8 +96,7 @@ public void setLazyDatabaseTransaction(boolean lazyDatabaseTransaction) { @Override - @Nullable - public Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) + public @Nullable Object beginTransaction(EntityManager entityManager, TransactionDefinition definition) throws PersistenceException, SQLException, TransactionException { int currentIsolationLevel = definition.getIsolationLevel(); @@ -172,8 +171,7 @@ private static class EclipseLinkConnectionHandle implements ConnectionHandle { private final EntityManager entityManager; - @Nullable - private Connection connection; + private @Nullable Connection connection; public EclipseLinkConnectionHandle(EntityManager entityManager) { this.entityManager = entityManager; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java index 4cbe6c71d7ea..84cfaac3e570 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/EclipseLinkJpaVendorAdapter.java @@ -25,8 +25,7 @@ import org.eclipse.persistence.config.PersistenceUnitProperties; import org.eclipse.persistence.config.TargetDatabase; import org.eclipse.persistence.jpa.JpaEntityManager; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for Eclipse @@ -91,8 +90,7 @@ public Map getJpaPropertyMap() { * @param database the specified database * @return the EclipseLink target database name, or {@code null} if none found */ - @Nullable - protected String determineTargetDatabaseName(Database database) { + protected @Nullable String determineTargetDatabaseName(Database database) { return switch (database) { case DB2 -> TargetDatabase.DB2; case DERBY -> TargetDatabase.Derby; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java index 12a9d75c97ba..196c22a3b2cd 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaDialect.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. @@ -47,6 +47,7 @@ import org.hibernate.exception.JDBCConnectionException; import org.hibernate.exception.LockAcquisitionException; import org.hibernate.exception.SQLGrammarException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.CannotAcquireLockException; import org.springframework.dao.DataAccessException; @@ -61,7 +62,6 @@ import org.springframework.jdbc.datasource.DataSourceUtils; import org.springframework.jdbc.support.SQLExceptionSubclassTranslator; import org.springframework.jdbc.support.SQLExceptionTranslator; -import org.springframework.lang.Nullable; import org.springframework.orm.ObjectOptimisticLockingFailureException; import org.springframework.orm.ObjectRetrievalFailureException; import org.springframework.orm.jpa.DefaultJpaDialect; @@ -89,11 +89,9 @@ public class HibernateJpaDialect extends DefaultJpaDialect { boolean prepareConnection = true; - @Nullable - private SQLExceptionTranslator jdbcExceptionTranslator; + private @Nullable SQLExceptionTranslator jdbcExceptionTranslator; - @Nullable - private SQLExceptionTranslator transactionExceptionTranslator = new SQLExceptionSubclassTranslator(); + private @Nullable SQLExceptionTranslator transactionExceptionTranslator = new SQLExceptionSubclassTranslator(); /** @@ -198,8 +196,7 @@ public Object prepareTransaction(EntityManager entityManager, boolean readOnly, return new SessionTransactionData(session, previousFlushMode, false, null, readOnly); } - @Nullable - protected FlushMode prepareFlushMode(Session session, boolean readOnly) throws PersistenceException { + protected @Nullable FlushMode prepareFlushMode(Session session, boolean readOnly) throws PersistenceException { FlushMode flushMode = session.getHibernateFlushMode(); if (readOnly) { // We should suppress flushing for a read-only transaction. @@ -235,8 +232,7 @@ public ConnectionHandle getJdbcConnection(EntityManager entityManager, boolean r } @Override - @Nullable - public DataAccessException translateExceptionIfPossible(RuntimeException ex) { + public @Nullable DataAccessException translateExceptionIfPossible(RuntimeException ex) { if (ex instanceof HibernateException hibernateEx) { return convertHibernateAccessException(hibernateEx); } @@ -253,14 +249,18 @@ public DataAccessException translateExceptionIfPossible(RuntimeException ex) { * @return the corresponding DataAccessException instance */ protected DataAccessException convertHibernateAccessException(HibernateException ex) { - if (this.jdbcExceptionTranslator != null && ex instanceof JDBCException jdbcEx) { + return convertHibernateAccessException(ex, ex); + } + + private DataAccessException convertHibernateAccessException(HibernateException ex, HibernateException exToCheck) { + if (this.jdbcExceptionTranslator != null && exToCheck instanceof JDBCException jdbcEx) { DataAccessException dae = this.jdbcExceptionTranslator.translate( "Hibernate operation: " + jdbcEx.getMessage(), jdbcEx.getSQL(), jdbcEx.getSQLException()); if (dae != null) { return dae; } } - if (this.transactionExceptionTranslator != null && ex instanceof org.hibernate.TransactionException) { + if (this.transactionExceptionTranslator != null && exToCheck instanceof org.hibernate.TransactionException) { if (ex.getCause() instanceof SQLException sqlEx) { DataAccessException dae = this.transactionExceptionTranslator.translate( "Hibernate transaction: " + ex.getMessage(), null, sqlEx); @@ -270,74 +270,77 @@ protected DataAccessException convertHibernateAccessException(HibernateException } } - if (ex instanceof JDBCConnectionException) { + if (exToCheck instanceof JDBCConnectionException) { return new DataAccessResourceFailureException(ex.getMessage(), ex); } - if (ex instanceof SQLGrammarException hibEx) { + if (exToCheck instanceof SQLGrammarException hibEx) { return new InvalidDataAccessResourceUsageException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof QueryTimeoutException hibEx) { + if (exToCheck instanceof QueryTimeoutException hibEx) { return new org.springframework.dao.QueryTimeoutException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof LockAcquisitionException hibEx) { + if (exToCheck instanceof LockAcquisitionException hibEx) { return new CannotAcquireLockException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof PessimisticLockException hibEx) { + if (exToCheck instanceof PessimisticLockException hibEx) { return new PessimisticLockingFailureException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } - if (ex instanceof ConstraintViolationException hibEx) { + if (exToCheck instanceof ConstraintViolationException hibEx) { return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]; constraint [" + hibEx.getConstraintName() + "]", ex); } - if (ex instanceof DataException hibEx) { + if (exToCheck instanceof DataException hibEx) { return new DataIntegrityViolationException(ex.getMessage() + "; SQL [" + hibEx.getSQL() + "]", ex); } // end of JDBCException subclass handling - if (ex instanceof QueryException) { + if (exToCheck instanceof QueryException) { return new InvalidDataAccessResourceUsageException(ex.getMessage(), ex); } - if (ex instanceof NonUniqueResultException) { + if (exToCheck instanceof NonUniqueResultException) { return new IncorrectResultSizeDataAccessException(ex.getMessage(), 1, ex); } - if (ex instanceof NonUniqueObjectException) { + if (exToCheck instanceof NonUniqueObjectException) { return new DuplicateKeyException(ex.getMessage(), ex); } - if (ex instanceof PropertyValueException) { + if (exToCheck instanceof PropertyValueException) { return new DataIntegrityViolationException(ex.getMessage(), ex); } - if (ex instanceof PersistentObjectException) { + if (exToCheck instanceof PersistentObjectException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof TransientObjectException) { + if (exToCheck instanceof TransientObjectException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof ObjectDeletedException) { + if (exToCheck instanceof ObjectDeletedException) { return new InvalidDataAccessApiUsageException(ex.getMessage(), ex); } - if (ex instanceof UnresolvableObjectException hibEx) { + if (exToCheck instanceof UnresolvableObjectException hibEx) { return new ObjectRetrievalFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof WrongClassException hibEx) { + if (exToCheck instanceof WrongClassException hibEx) { return new ObjectRetrievalFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof StaleObjectStateException hibEx) { + if (exToCheck instanceof StaleObjectStateException hibEx) { return new ObjectOptimisticLockingFailureException(hibEx.getEntityName(), getIdentifier(hibEx), ex.getMessage(), ex); } - if (ex instanceof StaleStateException) { + if (exToCheck instanceof StaleStateException) { return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex); } - if (ex instanceof OptimisticEntityLockException) { + if (exToCheck instanceof OptimisticEntityLockException) { return new ObjectOptimisticLockingFailureException(ex.getMessage(), ex); } - if (ex instanceof PessimisticEntityLockException) { + if (exToCheck instanceof PessimisticEntityLockException) { if (ex.getCause() instanceof LockAcquisitionException) { return new CannotAcquireLockException(ex.getMessage(), ex.getCause()); } return new PessimisticLockingFailureException(ex.getMessage(), ex); } - // fallback + // Fallback: check potentially more specific cause, otherwise JpaSystemException + if (exToCheck.getCause() instanceof HibernateException causeToCheck) { + return convertHibernateAccessException(ex, causeToCheck); + } return new JpaSystemException(ex); } @@ -345,8 +348,7 @@ protected SessionImplementor getSession(EntityManager entityManager) { return entityManager.unwrap(SessionImplementor.class); } - @Nullable - protected Object getIdentifier(HibernateException hibEx) { + protected @Nullable Object getIdentifier(HibernateException hibEx) { try { // getIdentifier declares Serializable return value on 5.x but Object on 6.x // -> not binary compatible, let's invoke it reflectively for the time being @@ -362,13 +364,11 @@ private static class SessionTransactionData { private final SessionImplementor session; - @Nullable - private final FlushMode previousFlushMode; + private final @Nullable FlushMode previousFlushMode; private final boolean needsConnectionReset; - @Nullable - private final Integer previousIsolationLevel; + private final @Nullable Integer previousIsolationLevel; private final boolean readOnly; diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java index 6f601c1ce80f..b8561cd3450c 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/HibernateJpaVendorAdapter.java @@ -28,22 +28,16 @@ import org.hibernate.SessionFactory; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.DB2Dialect; -import org.hibernate.dialect.DerbyTenSevenDialect; import org.hibernate.dialect.H2Dialect; -import org.hibernate.dialect.HANAColumnStoreDialect; +import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.HSQLDialect; -import org.hibernate.dialect.Informix10Dialect; -import org.hibernate.dialect.MySQL57Dialect; import org.hibernate.dialect.MySQLDialect; -import org.hibernate.dialect.Oracle12cDialect; -import org.hibernate.dialect.PostgreSQL95Dialect; -import org.hibernate.dialect.SQLServer2012Dialect; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.resource.jdbc.spi.PhysicalConnectionHandlingMode; - -import org.springframework.lang.Nullable; -import org.springframework.util.ClassUtils; +import org.jspecify.annotations.Nullable; /** * {@link org.springframework.orm.jpa.JpaVendorAdapter} implementation for Hibernate. @@ -72,9 +66,6 @@ */ public class HibernateJpaVendorAdapter extends AbstractJpaVendorAdapter { - private static final boolean oldDialectsPresent = ClassUtils.isPresent( - "org.hibernate.dialect.PostgreSQL95Dialect", HibernateJpaVendorAdapter.class.getClassLoader()); - private final HibernateJpaDialect jpaDialect = new HibernateJpaDialect(); private final PersistenceProvider persistenceProvider; @@ -129,6 +120,7 @@ public String getPersistenceProviderRootPackage() { return "org.hibernate"; } + @SuppressWarnings("removal") @Override public Map getJpaPropertyMap(PersistenceUnitInfo pui) { return buildJpaPropertyMap(this.jpaDialect.prepareConnection && @@ -151,6 +143,12 @@ private Map buildJpaPropertyMap(boolean connectionReleaseOnClose if (databaseDialectClass != null) { jpaProperties.put(AvailableSettings.DIALECT, databaseDialectClass.getName()); } + else { + String databaseDialectName = determineDatabaseDialectName(getDatabase()); + if (databaseDialectName != null) { + jpaProperties.put(AvailableSettings.DIALECT, databaseDialectName); + } + } } if (isGenerateDdl()) { @@ -173,43 +171,39 @@ private Map buildJpaPropertyMap(boolean connectionReleaseOnClose /** * Determine the Hibernate database dialect class for the given target database. + *

    The default implementation covers the common built-in dialects. * @param database the target database * @return the Hibernate database dialect class, or {@code null} if none found + * @see #determineDatabaseDialectName */ - @SuppressWarnings("deprecation") // for OracleDialect on Hibernate 5.6 and DerbyDialect/PostgreSQLDialect on Hibernate 6.2 - @Nullable - protected Class determineDatabaseDialectClass(Database database) { - if (oldDialectsPresent) { // Hibernate <6.2 - return switch (database) { - case DB2 -> DB2Dialect.class; - case DERBY -> DerbyTenSevenDialect.class; - case H2 -> H2Dialect.class; - case HANA -> HANAColumnStoreDialect.class; - case HSQL -> HSQLDialect.class; - case INFORMIX -> Informix10Dialect.class; - case MYSQL -> MySQL57Dialect.class; - case ORACLE -> Oracle12cDialect.class; - case POSTGRESQL -> PostgreSQL95Dialect.class; - case SQL_SERVER -> SQLServer2012Dialect.class; - case SYBASE -> SybaseDialect.class; - default -> null; - }; - } - else { // Hibernate 6.2+ aligned - return switch (database) { - case DB2 -> DB2Dialect.class; - case DERBY -> org.hibernate.dialect.DerbyDialect.class; - case H2 -> H2Dialect.class; - case HANA -> HANAColumnStoreDialect.class; - case HSQL -> HSQLDialect.class; - case MYSQL -> MySQLDialect.class; - case ORACLE -> org.hibernate.dialect.OracleDialect.class; - case POSTGRESQL -> org.hibernate.dialect.PostgreSQLDialect.class; - case SQL_SERVER -> SQLServerDialect.class; - case SYBASE -> SybaseDialect.class; - default -> null; - }; - } + protected @Nullable Class determineDatabaseDialectClass(Database database) { + return switch (database) { + case DB2 -> DB2Dialect.class; + case H2 -> H2Dialect.class; + case HANA -> HANADialect.class; + case HSQL -> HSQLDialect.class; + case MYSQL -> MySQLDialect.class; + case ORACLE -> OracleDialect.class; + case POSTGRESQL -> PostgreSQLDialect.class; + case SQL_SERVER -> SQLServerDialect.class; + case SYBASE -> SybaseDialect.class; + default -> null; + }; + } + + /** + * Determine the Hibernate database dialect class name for the given target database. + *

    The default implementation covers the common community dialect for Derby. + * @param database the target database + * @return the Hibernate database dialect class name, or {@code null} if none found + * @since 7.0 + * @see #determineDatabaseDialectClass + */ + protected @Nullable String determineDatabaseDialectName(Database database) { + return switch (database) { + case DERBY -> "org.hibernate.community.dialect.DerbyDialect"; + default -> null; + }; } @Override diff --git a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/package-info.java b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/package-info.java index b14f429ab986..10fb63e595b9 100644 --- a/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/jpa/vendor/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for adapting to specific JPA vendors. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm.jpa.vendor; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/main/java/org/springframework/orm/package-info.java b/spring-orm/src/main/java/org/springframework/orm/package-info.java index 2b6a01a34ffd..33e510e6a7c7 100644 --- a/spring-orm/src/main/java/org/springframework/orm/package-info.java +++ b/spring-orm/src/main/java/org/springframework/orm/package-info.java @@ -2,9 +2,7 @@ * Root package for Spring's O/R Mapping integration classes. * Contains generic DataAccessExceptions related to O/R Mapping. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.orm; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java index ac934635a2c1..14afaa64a832 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/DefaultJpaDialectTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-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,6 +19,7 @@ import jakarta.persistence.EntityManager; import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; import org.junit.jupiter.api.Test; import org.springframework.transaction.TransactionDefinition; @@ -33,33 +34,37 @@ /** * @author Costin Leau * @author Phillip Webb + * @author Juergen Hoeller */ class DefaultJpaDialectTests { - private JpaDialect dialect = new DefaultJpaDialect(); + private final JpaDialect dialect = new DefaultJpaDialect(); - @Test - void testDefaultTransactionDefinition() { - DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); - definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); - assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> - dialect.beginTransaction(null, definition)); - } @Test void testDefaultBeginTransaction() throws Exception { TransactionDefinition definition = new DefaultTransactionDefinition(); EntityManager entityManager = mock(); EntityTransaction entityTx = mock(); - given(entityManager.getTransaction()).willReturn(entityTx); dialect.beginTransaction(entityManager, definition); } + @Test + void testCustomIsolationLevel() { + DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_REPEATABLE_READ); + + assertThatExceptionOfType(TransactionException.class).isThrownBy(() -> + dialect.beginTransaction(null, definition)); + } + @Test void testTranslateException() { - OptimisticLockException ex = new OptimisticLockException(); - assertThat(dialect.translateExceptionIfPossible(ex).getCause()).isEqualTo(EntityManagerFactoryUtils.convertJpaAccessExceptionIfPossible(ex).getCause()); + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); } + } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java index 5b43a20dcd0d..3d17a76fab79 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/EntityManagerRuntimeHintsTests.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. @@ -60,7 +60,7 @@ void entityManagerProxyHasHibernateHints() { @Test void entityManagerFactoryHasReflectionHints() { - assertThat(RuntimeHintsPredicates.reflection().onMethod(EntityManagerFactory.class, "getCriteriaBuilder")).accepts(this.hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(EntityManagerFactory.class, "getMetamodel")).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(EntityManagerFactory.class, "getCriteriaBuilder")).accepts(this.hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(EntityManagerFactory.class, "getMetamodel")).accepts(this.hints); } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java index a31b2e646c3a..0752469e060b 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/LocalContainerEntityManagerFactoryBeanTests.java @@ -23,6 +23,7 @@ import jakarta.persistence.EntityManagerFactory; import jakarta.persistence.EntityTransaction; import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceConfiguration; import jakarta.persistence.PersistenceException; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceUnitInfo; @@ -52,7 +53,7 @@ * @author Juergen Hoeller * @author Phillip Webb */ -@SuppressWarnings("rawtypes") +@SuppressWarnings({"rawtypes", "removal"}) class LocalContainerEntityManagerFactoryBeanTests extends AbstractEntityManagerFactoryBeanTests { // Static fields set by inner class DummyPersistenceProvider @@ -310,6 +311,11 @@ public EntityManagerFactory createEntityManagerFactory(String emfName, Map prope throw new UnsupportedOperationException(); } + @Override + public EntityManagerFactory createEntityManagerFactory(PersistenceConfiguration persistenceConfiguration) { + throw new UnsupportedOperationException(); + } + @Override public ProviderUtil getProviderUtil() { throw new UnsupportedOperationException(); @@ -357,6 +363,15 @@ public boolean getRollbackOnly() { public boolean isActive() { return false; } + + @Override + public void setTimeout(Integer integer) { + } + + @Override + public Integer getTimeout() { + return null; + } } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBeanTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBeanTests.java index 81e2125b5dbd..379ecea804cb 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBeanTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/LocalEntityManagerFactoryBeanTests.java @@ -20,6 +20,7 @@ import java.util.Properties; import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.PersistenceConfiguration; import jakarta.persistence.spi.PersistenceProvider; import jakarta.persistence.spi.PersistenceUnitInfo; import jakarta.persistence.spi.ProviderUtil; @@ -97,6 +98,11 @@ public EntityManagerFactory createEntityManagerFactory(String emfName, Map prope return mockEmf; } + @Override + public EntityManagerFactory createEntityManagerFactory(PersistenceConfiguration persistenceConfiguration) { + throw new UnsupportedOperationException(); + } + @Override public ProviderUtil getProviderUtil() { throw new UnsupportedOperationException(); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java b/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java index cc701bff02af..c302d9580100 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/domain2/package-info.java @@ -1,7 +1,8 @@ /** * Sample package-info for testing purposes. */ -@TypeDef(name = "test", typeClass = Object.class) +@TypeRegistration(basicClass = Object.class, userType = UserTypeLegacyBridge.class) package org.springframework.orm.jpa.domain2; -import org.hibernate.annotations.TypeDef; +import org.hibernate.annotations.TypeRegistration; +import org.hibernate.usertype.UserTypeLegacyBridge; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java new file mode 100644 index 000000000000..02993f5061b0 --- /dev/null +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateJpaDialectTests.java @@ -0,0 +1,58 @@ +/* + * Copyright 2002-2025 the original author 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.orm.jpa.hibernate; + +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.PersistenceException; +import org.hibernate.HibernateException; +import org.hibernate.dialect.lock.OptimisticEntityLockException; +import org.junit.jupiter.api.Test; + +import org.springframework.orm.ObjectOptimisticLockingFailureException; +import org.springframework.orm.jpa.JpaDialect; +import org.springframework.orm.jpa.JpaOptimisticLockingFailureException; +import org.springframework.orm.jpa.vendor.HibernateJpaDialect; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Juergen Hoeller + */ +class HibernateJpaDialectTests { + + private final JpaDialect dialect = new HibernateJpaDialect(); + + + @Test + void testTranslateException() { + // Plain JPA exception + PersistenceException ex = new OptimisticLockException(); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(JpaOptimisticLockingFailureException.class).hasCause(ex); + + // Hibernate-specific exception + ex = new OptimisticEntityLockException("", ""); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(ObjectOptimisticLockingFailureException.class).hasCause(ex); + + // Nested Hibernate-specific exception + ex = new HibernateException(new OptimisticEntityLockException("", "")); + assertThat(dialect.translateExceptionIfPossible(ex)) + .isInstanceOf(ObjectOptimisticLockingFailureException.class).hasCause(ex); + } + +} diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java index 8da3a0e7b0e9..a84c539fb111 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactoryIntegrationTests.java @@ -53,11 +53,11 @@ protected String[] getConfigLocations() { } - @Override @Test + @Override protected void testEntityManagerFactoryImplementsEntityManagerFactoryInfo() { - boolean condition = entityManagerFactory instanceof EntityManagerFactoryInfo; - assertThat(condition).as("Must not have introduced config interface").isFalse(); + assertThat(entityManagerFactory).as("Must not have introduced config interface") + .isNotInstanceOf(EntityManagerFactoryInfo.class); } @Test @@ -66,23 +66,21 @@ public void testEntityListener() { String firstName = "Tony"; insertPerson(firstName); - List people = sharedEntityManager.createQuery("select p from Person as p").getResultList(); + List people = sharedEntityManager.createQuery("select p from Person as p", Person.class).getResultList(); assertThat(people).hasSize(1); assertThat(people.get(0).getFirstName()).isEqualTo(firstName); assertThat(people.get(0).postLoaded).isSameAs(applicationContext); } @Test - @SuppressWarnings({ "unchecked", "rawtypes" }) public void testCurrentSession() { String firstName = "Tony"; insertPerson(firstName); - Query q = sessionFactory.getCurrentSession().createQuery("select p from Person as p"); - List people = q.getResultList(); - assertThat(people).hasSize(1); - assertThat(people.get(0).getFirstName()).isEqualTo(firstName); - assertThat(people.get(0).postLoaded).isSameAs(applicationContext); + Query q = sessionFactory.getCurrentSession().createQuery("select p from Person as p", Person.class); + assertThat(q.getResultList()).hasSize(1); + assertThat(q.getResultList().get(0).getFirstName()).isEqualTo(firstName); + assertThat(q.getResultList().get(0).postLoaded).isSameAs(applicationContext); } @Test // SPR-16956 diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java index 1b3ce4087c97..52a9b2ca2ba0 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/hibernate/HibernateNativeEntityManagerFactorySpringBeanContainerIntegrationTests.java @@ -56,6 +56,7 @@ protected String[] getConfigLocations() { "/org/springframework/orm/jpa/hibernate/inject-hibernate-spring-bean-container-tests.xml"}; } + @SuppressWarnings("deprecation") private ManagedBeanRegistry getManagedBeanRegistry() { SessionFactory sessionFactory = entityManagerFactory.unwrap(SessionFactory.class); ServiceRegistry serviceRegistry = sessionFactory.getSessionFactoryOptions().getServiceRegistry(); diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java index 64f01029d07c..0b4e1d091817 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceManagedTypesBeanRegistrationAotProcessorTests.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,7 +21,7 @@ import javax.sql.DataSource; -import org.hibernate.tuple.CreationTimestampGeneration; +import org.hibernate.annotations.CreationTimestamp; import org.junit.jupiter.api.Test; import org.springframework.aot.hint.MemberCategory; @@ -85,18 +85,18 @@ void contributeJpaHints() { context.registerBean(JpaDomainConfiguration.class); contributeHints(context, hints -> { assertThat(RuntimeHintsPredicates.reflection().onType(DriversLicense.class) - .withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints); + .withMemberCategories(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(Person.class) - .withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints); + .withMemberCategories(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(PersonListener.class) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, MemberCategory.INVOKE_PUBLIC_METHODS)) .accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(Employee.class) - .withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints); - assertThat(RuntimeHintsPredicates.reflection().onMethod(Employee.class, "preRemove")) + .withMemberCategories(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(hints); + assertThat(RuntimeHintsPredicates.reflection().onMethodInvocation(Employee.class, "preRemove")) .accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeId.class) - .withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints); + .withMemberCategories(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeLocationConverter.class) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeCategoryConverter.class) @@ -104,16 +104,16 @@ void contributeJpaHints() { assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeKindConverter.class) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints); assertThat(RuntimeHintsPredicates.reflection().onType(EmployeeLocation.class) - .withMemberCategories(MemberCategory.DECLARED_FIELDS)).accepts(hints); + .withMemberCategories(MemberCategory.ACCESS_DECLARED_FIELDS)).accepts(hints); }); } - @Test + // @Test void contributeHibernateHints() { GenericApplicationContext context = new AnnotationConfigApplicationContext(); context.registerBean(HibernateDomainConfiguration.class); contributeHints(context, hints -> - assertThat(RuntimeHintsPredicates.reflection().onType(CreationTimestampGeneration.class) + assertThat(RuntimeHintsPredicates.reflection().onType(CreationTimestamp.class) .withMemberCategories(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS)).accepts(hints)); } @@ -144,6 +144,7 @@ private void contributeHints(GenericApplicationContext applicationContext, Consu result.accept(generationContext.getRuntimeHints()); } + public static class JpaDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration { @Override @@ -152,6 +153,7 @@ protected String packageToScan() { } } + public static class HibernateDomainConfiguration extends AbstractEntityManagerWithPackagesToScanConfiguration { @Override @@ -160,6 +162,7 @@ protected String packageToScan() { } } + public abstract static class AbstractEntityManagerWithPackagesToScanConfiguration { protected boolean scanningInvoked; @@ -194,7 +197,6 @@ public LocalContainerEntityManagerFactoryBean entityManagerFactory(DataSource da } protected abstract String packageToScan(); - } } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java index c0c66f1ce1b8..80c9fd987e9d 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/persistenceunit/PersistenceXmlParsingTests.java @@ -47,6 +47,7 @@ * @author Juergen Hoeller * @author Nicholas Williams */ +@SuppressWarnings("removal") class PersistenceXmlParsingTests { @Test diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java index 5aaa30088283..c54810d44ed4 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/InjectionCodeGeneratorTests.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. @@ -125,7 +125,7 @@ void generateCodeWhenPrivateFieldAddsHint() { Field field = ReflectionUtils.findField(bean.getClass(), "age"); createGenerator(TEST_TARGET).generateInjectionCode( field, INSTANCE_VARIABLE, CodeBlock.of("$L", 123)); - assertThat(RuntimeHintsPredicates.reflection().onField(TestBean.class, "age")) + assertThat(RuntimeHintsPredicates.reflection().onType(TestBean.class)) .accepts(this.hints); } @@ -197,7 +197,7 @@ void generateCodeWhenPrivateMethodAddsHint() { createGenerator(TEST_TARGET).generateInjectionCode( method, INSTANCE_VARIABLE, CodeBlock.of("$L", 123)); assertThat(RuntimeHintsPredicates.reflection() - .onMethod(TestBeanWithPrivateMethod.class, "setAge").invoke()).accepts(this.hints); + .onMethodInvocation(TestBeanWithPrivateMethod.class, "setAge")).accepts(this.hints); } private InjectionCodeGenerator createGenerator(ClassName target) { diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessorAotContributionTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessorAotContributionTests.java index cb31b8bf0142..78dc137844a4 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessorAotContributionTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceAnnotationBeanPostProcessorAotContributionTests.java @@ -29,6 +29,7 @@ import jakarta.persistence.PersistenceProperty; import jakarta.persistence.PersistenceUnit; import org.assertj.core.api.InstanceOfAssertFactories; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -43,7 +44,6 @@ import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.Compiled; import org.springframework.core.test.tools.TestCompiler; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -215,8 +215,7 @@ private void testCompile(RegisteredBean registeredBean, .compile(compiled -> result.accept(new Invoker(compiled), compiled)); } - @Nullable - private BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + private @Nullable BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { PersistenceAnnotationBeanPostProcessor postProcessor = new PersistenceAnnotationBeanPostProcessor(); return postProcessor.processAheadOfTime(registeredBean); } diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java index 877863a66ead..36c91e0bc790 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceContextTransactionTests.java @@ -22,11 +22,11 @@ import jakarta.persistence.PersistenceContext; import jakarta.persistence.PersistenceContextType; import jakarta.persistence.SynchronizationType; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.JpaTransactionManager; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.support.TransactionSynchronizationManager; diff --git a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionTests.java b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionTests.java index ade307a3b386..327da6edea4c 100644 --- a/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionTests.java +++ b/spring-orm/src/test/java/org/springframework/orm/jpa/support/PersistenceInjectionTests.java @@ -31,6 +31,7 @@ import jakarta.persistence.PersistenceProperty; import jakarta.persistence.PersistenceUnit; import org.hibernate.Session; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.FactoryBean; @@ -40,7 +41,6 @@ import org.springframework.context.testfixture.SimpleMapScope; import org.springframework.context.testfixture.jndi.ExpectedLookupTemplate; import org.springframework.core.testfixture.io.SerializationTestUtils; -import org.springframework.lang.Nullable; import org.springframework.orm.jpa.AbstractEntityManagerFactoryBeanTests; import org.springframework.orm.jpa.DefaultJpaDialect; import org.springframework.orm.jpa.EntityManagerFactoryInfo; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/config/package-info.java b/spring-oxm/src/main/java/org/springframework/oxm/config/package-info.java index 9907718e49ee..800e594cdefe 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/config/package-info.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/config/package-info.java @@ -1,9 +1,7 @@ /** * Provides an namespace handler for the Spring Object/XML namespace. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.oxm.config; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java index b989541fd35a..7bf0a5ee23fc 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/ClassPathJaxb2TypeScanner.java @@ -25,6 +25,7 @@ import jakarta.xml.bind.annotation.XmlRootElement; import jakarta.xml.bind.annotation.XmlSeeAlso; import jakarta.xml.bind.annotation.XmlType; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.Resource; import org.springframework.core.io.support.PathMatchingResourcePatternResolver; @@ -34,7 +35,6 @@ import org.springframework.core.type.classreading.MetadataReaderFactory; import org.springframework.core.type.filter.AnnotationTypeFilter; import org.springframework.core.type.filter.TypeFilter; -import org.springframework.lang.Nullable; import org.springframework.oxm.UncategorizedMappingException; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; 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 efddba004567..6f04669aa105 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 @@ -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. @@ -77,6 +77,7 @@ import jakarta.xml.bind.attachment.AttachmentUnmarshaller; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.w3c.dom.ls.LSResourceResolver; import org.xml.sax.EntityResolver; import org.xml.sax.InputSource; @@ -87,7 +88,6 @@ import org.springframework.beans.factory.InitializingBean; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.oxm.GenericMarshaller; import org.springframework.oxm.GenericUnmarshaller; import org.springframework.oxm.MarshallingFailureException; @@ -142,43 +142,31 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi /** Logger available to subclasses. */ protected final Log logger = LogFactory.getLog(getClass()); - @Nullable - private String contextPath; + private @Nullable String contextPath; - @Nullable - private Class[] classesToBeBound; + private Class @Nullable [] classesToBeBound; - @Nullable - private String[] packagesToScan; + private String @Nullable [] packagesToScan; - @Nullable - private Map jaxbContextProperties; + private @Nullable Map jaxbContextProperties; - @Nullable - private Map marshallerProperties; + private @Nullable Map marshallerProperties; - @Nullable - private Map unmarshallerProperties; + private @Nullable Map unmarshallerProperties; - @Nullable - private Marshaller.Listener marshallerListener; + private Marshaller.@Nullable Listener marshallerListener; - @Nullable - private Unmarshaller.Listener unmarshallerListener; + private Unmarshaller.@Nullable Listener unmarshallerListener; - @Nullable - private ValidationEventHandler validationEventHandler; + private @Nullable ValidationEventHandler validationEventHandler; - @Nullable - private XmlAdapter[] adapters; + private XmlAdapter @Nullable [] adapters; - @Nullable - private Resource[] schemaResources; + private Resource @Nullable [] schemaResources; private String schemaLanguage = XMLConstants.W3C_XML_SCHEMA_NS_URI; - @Nullable - private LSResourceResolver schemaResourceResolver; + private @Nullable LSResourceResolver schemaResourceResolver; private boolean lazyInit = false; @@ -188,29 +176,23 @@ public class Jaxb2Marshaller implements MimeMarshaller, MimeUnmarshaller, Generi private boolean checkForXmlRootElement = true; - @Nullable - private Class mappedClass; + private @Nullable Class mappedClass; - @Nullable - private ClassLoader beanClassLoader; + private @Nullable ClassLoader beanClassLoader; private final Lock jaxbContextLock = new ReentrantLock(); - @Nullable - private volatile JAXBContext jaxbContext; + private volatile @Nullable JAXBContext jaxbContext; - @Nullable - private Schema schema; + private @Nullable Schema schema; private boolean supportDtd = false; private boolean processExternalEntities = false; - @Nullable - private volatile SAXParserFactory schemaParserFactory; + private volatile @Nullable SAXParserFactory schemaParserFactory; - @Nullable - private volatile SAXParserFactory sourceParserFactory; + private volatile @Nullable SAXParserFactory sourceParserFactory; /** @@ -234,8 +216,7 @@ public void setContextPath(@Nullable String contextPath) { /** * Return the JAXB context path. */ - @Nullable - public String getContextPath() { + public @Nullable String getContextPath() { return this.contextPath; } @@ -244,15 +225,14 @@ public String getContextPath() { *

    Setting either this property, {@link #setContextPath "contextPath"} * or {@link #setPackagesToScan "packagesToScan"} is required. */ - public void setClassesToBeBound(@Nullable Class... classesToBeBound) { + public void setClassesToBeBound(Class @Nullable ... classesToBeBound) { this.classesToBeBound = classesToBeBound; } /** * Return the list of Java classes to be recognized by a newly created JAXBContext. */ - @Nullable - public Class[] getClassesToBeBound() { + public Class @Nullable [] getClassesToBeBound() { return this.classesToBeBound; } @@ -263,15 +243,14 @@ public Class[] getClassesToBeBound() { *

    Setting either this property, {@link #setContextPath "contextPath"} or * {@link #setClassesToBeBound "classesToBeBound"} is required. */ - public void setPackagesToScan(@Nullable String... packagesToScan) { + public void setPackagesToScan(String @Nullable ... packagesToScan) { this.packagesToScan = packagesToScan; } /** * Return the packages to search for JAXB2 annotations. */ - @Nullable - public String[] getPackagesToScan() { + public String @Nullable [] getPackagesToScan() { return this.packagesToScan; } @@ -644,7 +623,7 @@ public boolean supports(Type genericType) { parameterizedType.getActualTypeArguments().length == 1) { Type typeArgument = parameterizedType.getActualTypeArguments()[0]; if (typeArgument instanceof Class classArgument) { - return ((classArgument.isArray() && byte.class == classArgument.componentType()) || + return ((byte.class == classArgument.componentType()) || isPrimitiveWrapper(classArgument) || isStandardClass(classArgument) || supportsInternal(classArgument, false)); } diff --git a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/package-info.java b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/package-info.java index 8a630106f418..e9d62635a6ed 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/jaxb/package-info.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/jaxb/package-info.java @@ -2,9 +2,7 @@ * Package providing integration of JAXB * with Spring's O/X Mapping support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.oxm.jaxb; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeContainer.java b/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeContainer.java index 11f24456d0a7..9aa2b230da4d 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeContainer.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeContainer.java @@ -17,8 +17,7 @@ package org.springframework.oxm.mime; import jakarta.activation.DataHandler; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Represents a container for MIME attachments @@ -58,7 +57,6 @@ public interface MimeContainer { * @param contentId the content id * @return the attachment, as a data handler */ - @Nullable - DataHandler getAttachment(String contentId); + @Nullable DataHandler getAttachment(String contentId); } diff --git a/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeMarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeMarshaller.java index f22ab1632ecf..ad72e5eb1489 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeMarshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeMarshaller.java @@ -20,7 +20,8 @@ import javax.xml.transform.Result; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.oxm.Marshaller; import org.springframework.oxm.XmlMappingException; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeUnmarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeUnmarshaller.java index e45a3c35144d..2479d62ec3d4 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeUnmarshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/mime/MimeUnmarshaller.java @@ -20,7 +20,8 @@ import javax.xml.transform.Source; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.XmlMappingException; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/mime/package-info.java b/spring-oxm/src/main/java/org/springframework/oxm/mime/package-info.java index b62881733dea..180b9402d022 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/mime/package-info.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/mime/package-info.java @@ -1,9 +1,7 @@ /** * Contains (un)marshallers optimized to store binary data in MIME attachments. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.oxm.mime; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/package-info.java b/spring-oxm/src/main/java/org/springframework/oxm/package-info.java index a9774649b72d..09cff99872af 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/package-info.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/package-info.java @@ -3,9 +3,7 @@ * Contains generic Marshaller and Unmarshaller interfaces, * and XmlMappingExceptions related to O/X Mapping */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.oxm; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java index a3925acb5039..df150cc5edd8 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/support/AbstractMarshaller.java @@ -44,6 +44,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.xml.sax.ContentHandler; @@ -53,7 +54,6 @@ import org.xml.sax.XMLReader; import org.xml.sax.ext.LexicalHandler; -import org.springframework.lang.Nullable; import org.springframework.oxm.Marshaller; import org.springframework.oxm.Unmarshaller; import org.springframework.oxm.UnmarshallingFailureException; @@ -82,11 +82,9 @@ public abstract class AbstractMarshaller implements Marshaller, Unmarshaller { private boolean processExternalEntities = false; - @Nullable - private volatile DocumentBuilderFactory documentBuilderFactory; + private volatile @Nullable DocumentBuilderFactory documentBuilderFactory; - @Nullable - private volatile SAXParserFactory saxParserFactory; + private volatile @Nullable SAXParserFactory saxParserFactory; /** @@ -220,8 +218,7 @@ protected XMLReader createXmlReader() throws SAXException, ParserConfigurationEx * a byte stream, or {@code null} if none. *

    The default implementation returns {@code null}. */ - @Nullable - protected String getDefaultEncoding() { + protected @Nullable String getDefaultEncoding() { return null; } diff --git a/spring-oxm/src/main/java/org/springframework/oxm/support/MarshallingSource.java b/spring-oxm/src/main/java/org/springframework/oxm/support/MarshallingSource.java index e2b7882485c6..45f65624cb51 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/support/MarshallingSource.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/support/MarshallingSource.java @@ -22,6 +22,7 @@ import javax.xml.transform.sax.SAXResult; import javax.xml.transform.sax.SAXSource; +import org.jspecify.annotations.Nullable; import org.xml.sax.ContentHandler; import org.xml.sax.DTDHandler; import org.xml.sax.EntityResolver; @@ -33,7 +34,6 @@ import org.xml.sax.XMLReader; import org.xml.sax.ext.LexicalHandler; -import org.springframework.lang.Nullable; import org.springframework.oxm.Marshaller; import org.springframework.util.Assert; @@ -109,20 +109,15 @@ private static final class MarshallingXMLReader implements XMLReader { private final Object content; - @Nullable - private DTDHandler dtdHandler; + private @Nullable DTDHandler dtdHandler; - @Nullable - private ContentHandler contentHandler; + private @Nullable ContentHandler contentHandler; - @Nullable - private EntityResolver entityResolver; + private @Nullable EntityResolver entityResolver; - @Nullable - private ErrorHandler errorHandler; + private @Nullable ErrorHandler errorHandler; - @Nullable - private LexicalHandler lexicalHandler; + private @Nullable LexicalHandler lexicalHandler; private MarshallingXMLReader(Marshaller marshaller, Object content) { Assert.notNull(marshaller, "'marshaller' must not be null"); @@ -137,8 +132,7 @@ public void setContentHandler(@Nullable ContentHandler contentHandler) { } @Override - @Nullable - public ContentHandler getContentHandler() { + public @Nullable ContentHandler getContentHandler() { return this.contentHandler; } @@ -148,8 +142,7 @@ public void setDTDHandler(@Nullable DTDHandler dtdHandler) { } @Override - @Nullable - public DTDHandler getDTDHandler() { + public @Nullable DTDHandler getDTDHandler() { return this.dtdHandler; } @@ -159,8 +152,7 @@ public void setEntityResolver(@Nullable EntityResolver entityResolver) { } @Override - @Nullable - public EntityResolver getEntityResolver() { + public @Nullable EntityResolver getEntityResolver() { return this.entityResolver; } @@ -170,13 +162,11 @@ public void setErrorHandler(@Nullable ErrorHandler errorHandler) { } @Override - @Nullable - public ErrorHandler getErrorHandler() { + public @Nullable ErrorHandler getErrorHandler() { return this.errorHandler; } - @Nullable - protected LexicalHandler getLexicalHandler() { + protected @Nullable LexicalHandler getLexicalHandler() { return this.lexicalHandler; } @@ -191,8 +181,7 @@ public void setFeature(String name, boolean value) throws SAXNotRecognizedExcept } @Override - @Nullable - public Object getProperty(String name) throws SAXNotRecognizedException { + public @Nullable Object getProperty(String name) throws SAXNotRecognizedException { if ("http://xml.org/sax/properties/lexical-handler".equals(name)) { return this.lexicalHandler; } diff --git a/spring-oxm/src/main/java/org/springframework/oxm/support/SaxResourceUtils.java b/spring-oxm/src/main/java/org/springframework/oxm/support/SaxResourceUtils.java index b2e14d524887..73c63b5372a3 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/support/SaxResourceUtils.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/support/SaxResourceUtils.java @@ -18,10 +18,10 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; import org.xml.sax.InputSource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; /** * Convenient utility methods for dealing with SAX. @@ -51,8 +51,7 @@ public static InputSource createInputSource(Resource resource) throws IOExceptio * Retrieve the URL from the given resource as System ID. *

    Returns {@code null} if it cannot be opened. */ - @Nullable - private static String getSystemId(Resource resource) { + private static @Nullable String getSystemId(Resource resource) { try { return resource.getURI().toString(); } diff --git a/spring-oxm/src/main/java/org/springframework/oxm/support/package-info.java b/spring-oxm/src/main/java/org/springframework/oxm/support/package-info.java index e421943a51b7..fab4ceb1c0b7 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/support/package-info.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/support/package-info.java @@ -4,9 +4,7 @@ * with TrAX, MarshallingView for use within Spring Web MVC, and the * MarshallingMessageConverter for use within Spring's JMS support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.oxm.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java b/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java index a727bf1c2eaf..91a7bd47b8af 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/xstream/XStreamMarshaller.java @@ -66,6 +66,7 @@ import com.thoughtworks.xstream.mapper.MapperWrapper; import com.thoughtworks.xstream.security.ForbiddenClassException; import com.thoughtworks.xstream.security.TypePermission; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; @@ -76,7 +77,6 @@ import org.springframework.beans.factory.BeanClassLoaderAware; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.oxm.MarshallingFailureException; import org.springframework.oxm.UncategorizedMappingException; import org.springframework.oxm.UnmarshallingFailureException; @@ -130,60 +130,43 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo public static final String DEFAULT_ENCODING = "UTF-8"; - @Nullable - private ReflectionProvider reflectionProvider; + private @Nullable ReflectionProvider reflectionProvider; - @Nullable - private HierarchicalStreamDriver streamDriver; + private @Nullable HierarchicalStreamDriver streamDriver; - @Nullable - private HierarchicalStreamDriver defaultDriver; + private @Nullable HierarchicalStreamDriver defaultDriver; - @Nullable - private Mapper mapper; + private @Nullable Mapper mapper; - @Nullable - private Class[] mapperWrappers; + private Class @Nullable [] mapperWrappers; private ConverterLookup converterLookup = new DefaultConverterLookup(); private ConverterRegistry converterRegistry = (ConverterRegistry) this.converterLookup; - @Nullable - private ConverterMatcher[] converters; + private ConverterMatcher @Nullable [] converters; - @Nullable - private TypePermission[] typePermissions; + private TypePermission @Nullable [] typePermissions; - @Nullable - private MarshallingStrategy marshallingStrategy; + private @Nullable MarshallingStrategy marshallingStrategy; - @Nullable - private Integer mode; + private @Nullable Integer mode; - @Nullable - private Map aliases; + private @Nullable Map aliases; - @Nullable - private Map aliasesByType; + private @Nullable Map aliasesByType; - @Nullable - private Map fieldAliases; + private @Nullable Map fieldAliases; - @Nullable - private Class[] useAttributeForTypes; + private Class @Nullable [] useAttributeForTypes; - @Nullable - private Map useAttributeFor; + private @Nullable Map useAttributeFor; - @Nullable - private Map, String> implicitCollections; + private @Nullable Map, String> implicitCollections; - @Nullable - private Map, String> omittedFields; + private @Nullable Map, String> omittedFields; - @Nullable - private Class[] annotatedClasses; + private Class @Nullable [] annotatedClasses; private boolean autodetectAnnotations; @@ -191,11 +174,9 @@ public class XStreamMarshaller extends AbstractMarshaller implements BeanClassLo private NameCoder nameCoder = new XmlFriendlyNameCoder(); - @Nullable - private Class[] supportedClasses; + private Class @Nullable [] supportedClasses; - @Nullable - private ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); + private @Nullable ClassLoader beanClassLoader = ClassUtils.getDefaultClassLoader(); private final SingletonSupplier xstream = SingletonSupplier.of(this::buildXStream); diff --git a/spring-oxm/src/main/java/org/springframework/oxm/xstream/package-info.java b/spring-oxm/src/main/java/org/springframework/oxm/xstream/package-info.java index c01a9cbd1075..220ef5d73d6f 100644 --- a/spring-oxm/src/main/java/org/springframework/oxm/xstream/package-info.java +++ b/spring-oxm/src/main/java/org/springframework/oxm/xstream/package-info.java @@ -2,9 +2,7 @@ * Package providing integration of XStream * with Spring's O/X Mapping support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.oxm.xstream; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/spring-r2dbc.gradle b/spring-r2dbc/spring-r2dbc.gradle index 21b864e523fb..ccb9329cc7b5 100644 --- a/spring-r2dbc/spring-r2dbc.gradle +++ b/spring-r2dbc/spring-r2dbc.gradle @@ -8,6 +8,7 @@ dependencies { api(project(":spring-tx")) api("io.projectreactor:reactor-core") api("io.r2dbc:r2dbc-spi") + compileOnly("com.google.code.findbugs:jsr305") // for r2dbc-spi optional("org.jetbrains.kotlin:kotlin-reflect") optional("org.jetbrains.kotlin:kotlin-stdlib") optional("org.jetbrains.kotlinx:kotlinx-coroutines-core") diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/BadSqlGrammarException.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/BadSqlGrammarException.java index 84effd5a8f25..17a38a9e3c34 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/BadSqlGrammarException.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/BadSqlGrammarException.java @@ -17,9 +17,9 @@ package org.springframework.r2dbc; import io.r2dbc.spi.R2dbcException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessResourceUsageException; -import org.springframework.lang.Nullable; /** * Exception thrown when SQL specified is invalid. Such exceptions always have a @@ -53,8 +53,7 @@ public BadSqlGrammarException(String task, String sql, R2dbcException ex) { /** * Return the wrapped {@link R2dbcException}. */ - @Nullable - public R2dbcException getR2dbcException() { + public @Nullable R2dbcException getR2dbcException() { return (R2dbcException) getCause(); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/UncategorizedR2dbcException.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/UncategorizedR2dbcException.java index 3c0b6acfa709..43fb1f5c3506 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/UncategorizedR2dbcException.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/UncategorizedR2dbcException.java @@ -17,9 +17,9 @@ package org.springframework.r2dbc; import io.r2dbc.spi.R2dbcException; +import org.jspecify.annotations.Nullable; import org.springframework.dao.UncategorizedDataAccessException; -import org.springframework.lang.Nullable; /** * Exception thrown when we can't classify a {@link R2dbcException} into @@ -32,8 +32,7 @@ public class UncategorizedR2dbcException extends UncategorizedDataAccessException { /** SQL that led to the problem. */ - @Nullable - private final String sql; + private final @Nullable String sql; /** @@ -51,16 +50,14 @@ public UncategorizedR2dbcException(String msg, @Nullable String sql, R2dbcExcept /** * Return the wrapped {@link R2dbcException}. */ - @Nullable - public R2dbcException getR2dbcException() { + public @Nullable R2dbcException getR2dbcException() { return (R2dbcException) getCause(); } /** * Return the SQL that led to the problem (if known). */ - @Nullable - public String getSql() { + public @Nullable String getSql() { return this.sql; } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java index 847dd93b725d..81a4819c2edc 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionFactoryUtils.java @@ -31,6 +31,7 @@ import io.r2dbc.spi.R2dbcTransientException; import io.r2dbc.spi.R2dbcTransientResourceException; import io.r2dbc.spi.Wrapped; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.Ordered; @@ -43,7 +44,6 @@ import org.springframework.dao.PessimisticLockingFailureException; import org.springframework.dao.QueryTimeoutException; import org.springframework.dao.TransientDataAccessResourceException; -import org.springframework.lang.Nullable; import org.springframework.r2dbc.BadSqlGrammarException; import org.springframework.r2dbc.UncategorizedR2dbcException; import org.springframework.transaction.NoTransactionException; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionHolder.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionHolder.java index dd1b9c36c718..7f5b1a45eea3 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionHolder.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/ConnectionHolder.java @@ -18,8 +18,8 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.transaction.support.ResourceHolderSupport; import org.springframework.util.Assert; @@ -48,8 +48,7 @@ public class ConnectionHolder extends ResourceHolderSupport { static final String SAVEPOINT_NAME_PREFIX = "SAVEPOINT_"; - @Nullable - private Connection currentConnection; + private @Nullable Connection currentConnection; private boolean transactionActive; 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 d7dd814f6e3c..da2ed7128c79 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. @@ -24,10 +24,10 @@ import io.r2dbc.spi.Option; import io.r2dbc.spi.R2dbcException; import io.r2dbc.spi.Result; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.TransactionDefinition; import org.springframework.transaction.reactive.AbstractReactiveTransactionManager; @@ -83,8 +83,7 @@ @SuppressWarnings("serial") public class R2dbcTransactionManager extends AbstractReactiveTransactionManager implements InitializingBean { - @Nullable - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; private boolean enforceReadOnly = false; @@ -123,8 +122,7 @@ public void setConnectionFactory(@Nullable ConnectionFactory connectionFactory) /** * Return the R2DBC {@link ConnectionFactory} that this instance manages transactions for. */ - @Nullable - public ConnectionFactory getConnectionFactory() { + public @Nullable ConnectionFactory getConnectionFactory() { return this.connectionFactory; } @@ -221,7 +219,7 @@ protected Mono doBegin(TransactionSynchronizationManager synchronizationMa 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)) @@ -296,7 +294,7 @@ protected Mono doResume(TransactionSynchronizationManager synchronizationM } @Override - protected Mono doCommit(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doCommit(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); @@ -308,7 +306,7 @@ protected Mono doCommit(TransactionSynchronizationManager TransactionSynch } @Override - protected Mono doRollback(TransactionSynchronizationManager TransactionSynchronizationManager, + protected Mono doRollback(TransactionSynchronizationManager synchronizationManager, GenericReactiveTransaction status) { ConnectionFactoryTransactionObject txObject = (ConnectionFactoryTransactionObject) status.getTransaction(); @@ -415,8 +413,7 @@ protected Mono prepareTransactionalConnection(Connection con, TransactionD * should remain {@link TransactionDefinition#ISOLATION_DEFAULT default}. * @see TransactionDefinition#getIsolationLevel() */ - @Nullable - protected IsolationLevel resolveIsolationLevel(int isolationLevel) { + protected @Nullable IsolationLevel resolveIsolationLevel(int isolationLevel) { return switch (isolationLevel) { case TransactionDefinition.ISOLATION_READ_COMMITTED -> IsolationLevel.READ_COMMITTED; case TransactionDefinition.ISOLATION_READ_UNCOMMITTED -> IsolationLevel.READ_UNCOMMITTED; @@ -448,13 +445,11 @@ private record ExtendedTransactionDefinition(@Nullable String transactionName, @SuppressWarnings("unchecked") @Override - @Nullable - public T getAttribute(Option option) { + public @Nullable T getAttribute(Option option) { return (T) doGetValue(option); } - @Nullable - private Object doGetValue(Option option) { + private @Nullable Object doGetValue(Option option) { if (io.r2dbc.spi.TransactionDefinition.ISOLATION_LEVEL.equals(option)) { return this.isolationLevel; } @@ -491,15 +486,13 @@ public String toString() { */ private static class ConnectionFactoryTransactionObject { - @Nullable - private ConnectionHolder connectionHolder; + private @Nullable ConnectionHolder connectionHolder; private boolean newConnectionHolder; private boolean mustRestoreAutoCommit; - @Nullable - private String savepointName; + private @Nullable String savepointName; void setConnectionHolder(@Nullable ConnectionHolder connectionHolder, boolean newConnectionHolder) { setConnectionHolder(connectionHolder); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java index 65e9786f8b1e..c31a9a309df8 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/SingleConnectionFactory.java @@ -27,11 +27,11 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; import io.r2dbc.spi.Wrapped; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Mono; import org.springframework.beans.factory.DisposableBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -70,15 +70,13 @@ public class SingleConnectionFactory extends DelegatingConnectionFactory private boolean suppressClose; /** Override auto-commit state?. */ - @Nullable - private Boolean autoCommit; + private @Nullable Boolean autoCommit; /** Wrapped Connection. */ private final AtomicReference target = new AtomicReference<>(); /** Proxy Connection. */ - @Nullable - private Connection connection; + private @Nullable Connection connection; private final Mono connectionEmitter; @@ -164,8 +162,7 @@ public void setAutoCommit(boolean autoCommit) { * be overridden. * @return the "autoCommit" value, or {@code null} if none to be applied */ - @Nullable - protected Boolean getAutoCommitValue() { + protected @Nullable Boolean getAutoCommitValue() { return this.autoCommit; } @@ -251,8 +248,7 @@ private static class CloseSuppressingInvocationHandler implements InvocationHand } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return switch (method.getName()) { // Only consider equal when proxies are identical. case "equals" -> proxy == args[0]; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxy.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxy.java index c9d47089e877..7ff8ecc15052 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxy.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxy.java @@ -24,9 +24,9 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Wrapped; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; /** @@ -129,8 +129,7 @@ private static class TransactionAwareInvocationHandler implements InvocationHand } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (ReflectionUtils.isObjectMethod(method)) { if (ReflectionUtils.isToStringMethod(method)) { return proxyToString(proxy); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializer.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializer.java index bfd89a9596c3..9aabcf8a9d38 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializer.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializer.java @@ -17,10 +17,10 @@ package org.springframework.r2dbc.connection.init; import io.r2dbc.spi.ConnectionFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.DisposableBean; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -36,14 +36,11 @@ */ public class ConnectionFactoryInitializer implements InitializingBean, DisposableBean { - @Nullable - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; - @Nullable - private DatabasePopulator databasePopulator; + private @Nullable DatabasePopulator databasePopulator; - @Nullable - private DatabasePopulator databaseCleaner; + private @Nullable DatabasePopulator databaseCleaner; private boolean enabled = true; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java index 86dc2f4fcf23..3f7d2b188c88 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulator.java @@ -22,6 +22,7 @@ import java.util.List; import io.r2dbc.spi.Connection; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -29,7 +30,6 @@ import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -59,8 +59,7 @@ public class ResourceDatabasePopulator implements DatabasePopulator { List scripts = new ArrayList<>(); - @Nullable - private Charset sqlScriptEncoding; + private @Nullable Charset sqlScriptEncoding; private String separator = ScriptUtils.DEFAULT_STATEMENT_SEPARATOR; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptException.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptException.java index 17491c476aff..8e8b0e97a931 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptException.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptException.java @@ -16,8 +16,9 @@ package org.springframework.r2dbc.connection.init; +import org.jspecify.annotations.Nullable; + import org.springframework.dao.DataAccessException; -import org.springframework.lang.Nullable; /** * Root of the hierarchy of data access exceptions that are related to processing diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptParseException.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptParseException.java index 1b9d6883a9f7..aba165b5ebae 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptParseException.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptParseException.java @@ -16,8 +16,9 @@ package org.springframework.r2dbc.connection.init; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; /** * Thrown by {@link ScriptUtils} if an SQL script cannot be properly parsed. diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java index 8bf20593e2e5..abe44653975f 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/ScriptUtils.java @@ -29,6 +29,7 @@ import io.r2dbc.spi.Result; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -38,7 +39,6 @@ import org.springframework.core.io.buffer.DataBufferUtils; import org.springframework.core.io.buffer.DefaultDataBufferFactory; import org.springframework.core.io.support.EncodedResource; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/package-info.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/package-info.java index e28bcfa4428e..9e01277a129b 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/package-info.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/init/package-info.java @@ -1,9 +1,7 @@ /** * Provides extensible support for initializing databases through scripts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.r2dbc.connection.init; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java index 900197f5c0cb..21af276a5846 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/AbstractRoutingConnectionFactory.java @@ -21,10 +21,10 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.beans.factory.InitializingBean; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -54,21 +54,17 @@ public abstract class AbstractRoutingConnectionFactory implements ConnectionFact private static final Object FALLBACK_MARKER = new Object(); - @Nullable - private Map targetConnectionFactories; + private @Nullable Map targetConnectionFactories; - @Nullable - private Object defaultTargetConnectionFactory; + private @Nullable Object defaultTargetConnectionFactory; private boolean lenientFallback = true; private ConnectionFactoryLookup connectionFactoryLookup = new MapConnectionFactoryLookup(); - @Nullable - private Map resolvedConnectionFactories; + private @Nullable Map resolvedConnectionFactories; - @Nullable - private ConnectionFactory resolvedDefaultConnectionFactory; + private @Nullable ConnectionFactory resolvedDefaultConnectionFactory; /** @@ -142,7 +138,6 @@ public void afterPropertiesSet() { * @see #setTargetConnectionFactories(Map) * @see #setDefaultTargetConnectionFactory(Object) */ - @SuppressWarnings("NullAway") public void initialize() { Assert.notNull(this.targetConnectionFactories, "Property 'targetConnectionFactories' must not be null"); @@ -221,7 +216,7 @@ public ConnectionFactoryMetadata getMetadata() { * per {@link #determineCurrentLookupKey()} * @see #determineCurrentLookupKey() */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Lambda protected Mono determineTargetConnectionFactory() { Assert.state(this.resolvedConnectionFactories != null, "ConnectionFactory router not initialized"); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookup.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookup.java index 5359fec11258..96f48d4a09df 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookup.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookup.java @@ -17,11 +17,11 @@ package org.springframework.r2dbc.connection.lookup; import io.r2dbc.spi.ConnectionFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryAware; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -37,8 +37,7 @@ */ public class BeanFactoryConnectionFactoryLookup implements ConnectionFactoryLookup, BeanFactoryAware { - @Nullable - private BeanFactory beanFactory; + private @Nullable BeanFactory beanFactory; /** diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/package-info.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/package-info.java index cd99f598865d..499e6c13c96c 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/package-info.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/lookup/package-info.java @@ -1,9 +1,7 @@ /** * Provides a strategy for looking up R2DBC ConnectionFactories by name. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.r2dbc.connection.lookup; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/package-info.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/package-info.java index 5da1a594ea92..dbeb5f8bcdac 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/package-info.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/connection/package-info.java @@ -3,9 +3,7 @@ * a ReactiveTransactionManager for a single ConnectionFactory, * and various simple ConnectionFactory implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.r2dbc.connection; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BeanPropertyRowMapper.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BeanPropertyRowMapper.java index fbae6633ad89..6e31088d1eb6 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BeanPropertyRowMapper.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/BeanPropertyRowMapper.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. @@ -29,13 +29,13 @@ import io.r2dbc.spi.ReadableMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeanWrapperImpl; import org.springframework.beans.TypeConverter; import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.support.DefaultConversionService; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -125,7 +125,7 @@ public BeanPropertyRowMapper(Class mappedClass, ConversionService conversionS * Remove the specified property from the mapped properties. * @param propertyName the property name (as used by property descriptors) */ - protected void suppressProperty(String propertyName) { + protected void suppressProperty(@Nullable String propertyName) { this.mappedProperties.remove(lowerCaseName(propertyName)); this.mappedProperties.remove(underscoreName(propertyName)); } @@ -136,7 +136,10 @@ protected void suppressProperty(String propertyName) { * @param name the original name * @return the converted name */ - protected String lowerCaseName(String name) { + protected String lowerCaseName(@Nullable String name) { + if (!StringUtils.hasLength(name)) { + return ""; + } return name.toLowerCase(Locale.US); } @@ -147,7 +150,7 @@ protected String lowerCaseName(String name) { * @return the converted name * @see #lowerCaseName */ - protected String underscoreName(String name) { + protected String underscoreName(@Nullable String name) { if (!StringUtils.hasLength(name)) { return ""; } @@ -235,8 +238,7 @@ protected T constructMappedInstance(Readable readable, List paramType) { + protected @Nullable Object getItemValue(Readable readable, int itemIndex, Class paramType) { try { return readable.get(itemIndex, paramType); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java index 5382452bd0a9..0c060e3fe398 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ColumnMapRowMapper.java @@ -23,8 +23,8 @@ import io.r2dbc.spi.ColumnMetadata; import io.r2dbc.spi.Row; import io.r2dbc.spi.RowMetadata; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.LinkedCaseInsensitiveMap; /** @@ -95,8 +95,7 @@ protected String getColumnKey(String columnName) { * @param index is the column index * @return the Object returned */ - @Nullable - protected Object getColumnValue(Row row, int index) { + protected @Nullable Object getColumnValue(Row row, int index) { return row.get(index); } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java index c0b6af51a79c..facc258f720a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DataClassRowMapper.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 io.r2dbc.spi.Readable; import io.r2dbc.spi.ReadableMetadata; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.TypeConverter; @@ -66,7 +67,7 @@ public class DataClassRowMapper extends BeanPropertyRowMapper { private final Constructor mappedConstructor; - private final String[] constructorParameterNames; + private final @Nullable String[] constructorParameterNames; private final TypeDescriptor[] constructorParameterTypes; @@ -98,7 +99,7 @@ public DataClassRowMapper(Class mappedClass, ConversionService conversionServ @Override protected T constructMappedInstance(Readable readable, List itemMetadatas, TypeConverter tc) { - Object[] args = new Object[this.constructorParameterNames.length]; + @Nullable Object[] args = new Object[this.constructorParameterNames.length]; for (int i = 0; i < args.length; i++) { String name = this.constructorParameterNames[i]; int index = findIndex(itemMetadatas, lowerCaseName(name)); diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java index d535a3989b6a..35f44b4e2661 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClient.java @@ -45,13 +45,13 @@ import io.r2dbc.spi.Wrapped; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import org.springframework.beans.BeanUtils; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; import org.springframework.r2dbc.connection.ConnectionFactoryUtils; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.r2dbc.core.binding.BindTarget; @@ -82,8 +82,7 @@ final class DefaultDatabaseClient implements DatabaseClient { private final ExecuteFunction executeFunction; - @Nullable - private final NamedParameterExpander namedParameterExpander; + private final @Nullable NamedParameterExpander namedParameterExpander; DefaultDatabaseClient(BindMarkersFactory bindMarkersFactory, ConnectionFactory connectionFactory, @@ -208,8 +207,7 @@ private static Mono sumRowsUpdated(Function> resu * @return the SQL string, or {@code null} * @see SqlProvider */ - @Nullable - private static String getSql(Object object) { + private static @Nullable String getSql(Object object) { if (object instanceof SqlProvider sqlProvider) { return sqlProvider.getSql(); } @@ -475,8 +473,7 @@ private MapBindParameterSource retrieveParameters(String sql, List param return new MapBindParameterSource(namedBindings); } - @Nullable - private Parameter getParameter(Map remainderByName, + private @Nullable Parameter getParameter(Map remainderByName, Map remainderByIndex, List parameterNames, String parameterName) { if (this.byName.containsKey(parameterName)) { @@ -530,8 +527,7 @@ private static class CloseSuppressingInvocationHandler implements InvocationHand } @Override - @Nullable - public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + public @Nullable Object invoke(Object proxy, Method method, Object[] args) throws Throwable { return switch (method.getName()) { // Only consider equal when proxies are identical. case "equals" -> proxy == args[0]; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClientBuilder.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClientBuilder.java index c8811043aed3..a93ffb3bc854 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClientBuilder.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DefaultDatabaseClientBuilder.java @@ -20,8 +20,8 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.Statement; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.r2dbc.core.binding.BindMarkersFactoryResolver; import org.springframework.util.Assert; @@ -34,11 +34,9 @@ */ class DefaultDatabaseClientBuilder implements DatabaseClient.Builder { - @Nullable - private BindMarkersFactory bindMarkers; + private @Nullable BindMarkersFactory bindMarkers; - @Nullable - private ConnectionFactory connectionFactory; + private @Nullable ConnectionFactory connectionFactory; private ExecuteFunction executeFunction = Statement::execute; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java index a25bddd7f665..1be2d1cf1052 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/DelegateConnectionFunction.java @@ -19,8 +19,7 @@ import java.util.function.Function; import io.r2dbc.spi.Connection; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A {@link ConnectionFunction} that delegates to a {@code SqlProvider} and a plain @@ -48,9 +47,8 @@ public R apply(Connection t) { return this.function.apply(t); } - @Nullable @Override - public String getSql() { + public @Nullable String getSql() { return this.sql.getSql(); } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java index 826666d74b4a..5c416284c0c5 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/MapBindParameterSource.java @@ -75,7 +75,7 @@ public boolean hasValue(String paramName) { } @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Parameter getValue(String paramName) throws IllegalArgumentException { if (!hasValue(paramName)) { throw new IllegalArgumentException("No value registered for key '" + paramName + "'"); 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 f6696e8d9cd9..5c48b9972859 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. @@ -26,9 +26,9 @@ import java.util.TreeMap; import io.r2dbc.spi.Parameter; +import org.jspecify.annotations.Nullable; import org.springframework.dao.InvalidDataAccessApiUsageException; -import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.binding.BindMarker; import org.springframework.r2dbc.core.binding.BindMarkers; import org.springframework.r2dbc.core.binding.BindMarkersFactory; @@ -49,6 +49,7 @@ * @author Juergen Hoeller * @author Mark Paluch * @author Anton Naydenov + * @author Sam Brannen * @since 5.3 */ abstract class NamedParameterUtils { @@ -460,8 +461,7 @@ NamedParameter getOrCreate(String namedParameter) { return param; } - @Nullable - List getMarker(String name) { + @Nullable List getMarker(String name) { return this.references.get(name); } @@ -513,37 +513,75 @@ private static class ExpandedQuery implements PreparedOperation { 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); + + @Override + public String toQuery() { + return this.expandedSql; + } + + @Override + public String getSource() { + return this.expandedSql; + } + + @Override + public void bindTo(BindTarget target) { + for (String namedParameter : this.parameterSource.getParameterNames()) { + Parameter parameter = this.parameterSource.getValue(namedParameter); + if (parameter.getValue() == null) { + bindNull(target, namedParameter, parameter); + } + else { + bind(target, namedParameter, parameter); + } + } + } + + 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; } - 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); + + 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 { - bind(target, markers, valueToBind); } } - } - else { - for (BindMarker bindMarker : bindMarkers) { - bindMarker.bind(target, parameter); + else { + for (BindMarker bindMarker : outer) { + bindMarker.bind(target, parameter); + } } } } @@ -555,52 +593,18 @@ private void bind(BindTarget target, Iterator markers, Object valueT 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) { + private @Nullable List> getBindMarkers(String identifier) { List parameters = this.parameters.getMarker(identifier); if (parameters == null) { return null; } - List markers = new ArrayList<>(); + List> markers = new ArrayList<>(); for (NamedParameters.NamedParameter parameter : parameters) { - markers.addAll(parameter.placeholders); + markers.add(new ArrayList<>(parameter.placeholders)); } return markers; } - @Override - public String getSource() { - return this.expandedSql; - } - - @Override - public void bindTo(BindTarget target) { - for (String namedParameter : this.parameterSource.getParameterNames()) { - Parameter parameter = this.parameterSource.getValue(namedParameter); - if (parameter.getValue() == null) { - bindNull(target, namedParameter, parameter); - } - else { - bind(target, namedParameter, parameter); - } - } - } - - @Override - public String toQuery() { - return this.expandedSql; - } } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java index 7d0d482fac13..b5b892d1de00 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/Parameter.java @@ -16,7 +16,8 @@ package org.springframework.r2dbc.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -32,8 +33,7 @@ @Deprecated(since = "6.0") public final class Parameter { - @Nullable - private final Object value; + private final @Nullable Object value; private final Class type; @@ -79,8 +79,7 @@ public static Parameter empty(Class type) { * Return the column value (can be {@code null}). * @see #hasValue() */ - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return this.value; } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java index 1204eac5df01..1dbdb45b880a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/ResultFunction.java @@ -22,9 +22,9 @@ import io.r2dbc.spi.Connection; import io.r2dbc.spi.Result; import io.r2dbc.spi.Statement; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -46,8 +46,7 @@ final class ResultFunction implements ConnectionFunction> { final StatementFilterFunction filterFunction; final ExecuteFunction executeFunction; - @Nullable - String resolvedSql = null; + @Nullable String resolvedSql = null; ResultFunction(Supplier sqlSupplier, BiFunction statementFunction, StatementFilterFunction filterFunction, ExecuteFunction executeFunction) { this.sqlSupplier = sqlSupplier; @@ -66,9 +65,8 @@ public Flux apply(Connection connection) { .cast(Result.class).checkpoint("SQL \"" + sql + "\" [DatabaseClient]"); } - @Nullable @Override - public String getSql() { + public @Nullable String getSql() { return this.resolvedSql; } } diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/SqlProvider.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/SqlProvider.java index a46c98af84ca..3b8c84d2707a 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/SqlProvider.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/SqlProvider.java @@ -16,7 +16,7 @@ package org.springframework.r2dbc.core; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Interface to be implemented by objects that can provide SQL strings. @@ -35,7 +35,6 @@ public interface SqlProvider { * typically the SQL used for creating statements. * @return the SQL string, or {@code null} if not available */ - @Nullable - String getSql(); + @Nullable String getSql(); } 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 71569702ccc7..c66ada2ca172 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 @@ 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 5fb53c47c190..40f8a71313e6 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 @@ /** * 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 af5a9fd48ca2..5504cbc01e1d 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 @@ * 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 c658352846d7..bac723f4c337 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. @@ -22,16 +22,16 @@ import io.r2dbc.spi.ConnectionFactory; import io.r2dbc.spi.ConnectionFactoryMetadata; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.support.SpringFactoriesLoader; import org.springframework.dao.NonTransientDataAccessException; -import org.springframework.lang.Nullable; 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,27 +69,29 @@ private 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); + @Nullable BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory); } /** - * Exception thrown when {@link BindMarkersFactoryResolver} cannot resolve a + * Exception thrown when a {@link BindMarkersFactoryResolver} cannot resolve a * {@link BindMarkersFactory}. */ @SuppressWarnings("serial") @@ -106,8 +108,11 @@ public NoBindMarkersFactoryException(String msg) { /** - * 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 */ @@ -129,8 +134,7 @@ static class BuiltInBindMarkersFactoryProvider implements BindMarkerFactoryProvi @Override - @Nullable - public BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory) { + public @Nullable BindMarkersFactory getBindMarkers(ConnectionFactory connectionFactory) { ConnectionFactoryMetadata metadata = connectionFactory.getMetadata(); BindMarkersFactory r2dbcDialect = BUILTIN.get(metadata.getName()); if (r2dbcDialect != null) { diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/Bindings.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/Bindings.java index a0a6eacc737b..36dd071e45a8 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/Bindings.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/Bindings.java @@ -26,9 +26,8 @@ import java.util.function.Consumer; import io.r2dbc.spi.Statement; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; @@ -178,8 +177,7 @@ public boolean isNull() { * @return the value of this binding * (can be {@code null} if this is a {@code NULL} binding) */ - @Nullable - public abstract Object getValue(); + public abstract @Nullable Object getValue(); /** * Apply the binding to a {@link BindTarget}. @@ -207,7 +205,6 @@ public boolean hasValue() { } @Override - @NonNull public Object getValue() { return this.value; } @@ -237,8 +234,7 @@ public boolean hasValue() { } @Override - @Nullable - public Object getValue() { + public @Nullable Object getValue() { return null; } 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 5f14d937b4f1..23081c9778cb 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 @@ 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/main/java/org/springframework/r2dbc/core/binding/package-info.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/package-info.java index e95acdc21d77..36ea3a687597 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/package-info.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/binding/package-info.java @@ -1,9 +1,7 @@ /** * Classes providing an abstraction over SQL bind markers. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.r2dbc.core.binding; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/package-info.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/package-info.java index a85478547b10..78aa55e025f0 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/package-info.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/core/package-info.java @@ -1,9 +1,7 @@ /** * Core domain types around DatabaseClient. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.r2dbc.core; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/src/main/java/org/springframework/r2dbc/package-info.java b/spring-r2dbc/src/main/java/org/springframework/r2dbc/package-info.java index c892eec7f380..dc32fb63973c 100644 --- a/spring-r2dbc/src/main/java/org/springframework/r2dbc/package-info.java +++ b/spring-r2dbc/src/main/java/org/springframework/r2dbc/package-info.java @@ -13,9 +13,7 @@ * dependencies into application code. * */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.r2dbc; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryTests.java index 8f120de9e433..a0cb5232461b 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/DelegatingConnectionFactoryTests.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,8 +22,8 @@ import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link DelegatingConnectionFactory}. diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/R2dbcTransactionManagerTests.java index 040c4359cbeb..b383b633b01b 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 @@ -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.IsolationLevel; import io.r2dbc.spi.R2dbcBadGrammarException; +import io.r2dbc.spi.R2dbcTransientResourceException; import io.r2dbc.spi.Statement; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -32,6 +33,7 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; +import org.springframework.dao.TransientDataAccessResourceException; import org.springframework.r2dbc.BadSqlGrammarException; import org.springframework.transaction.CannotCreateTransactionException; import org.springframework.transaction.IllegalTransactionStateException; @@ -46,13 +48,13 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.inOrder; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.never; -import static org.mockito.BDDMockito.reset; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.verifyNoMoreInteractions; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; /** * Tests for {@link R2dbcTransactionManager}. @@ -315,6 +317,31 @@ void testConnectionReleasedWhenRollbackFails() { verify(connectionMock).close(); } + @Test + void testCommitAndRollbackFails() { + when(connectionMock.isAutoCommit()).thenReturn(false); + when(connectionMock.commitTransaction()).thenReturn(Mono.defer(() -> + Mono.error(new R2dbcBadGrammarException("Commit should fail")))); + when(connectionMock.rollbackTransaction()).thenReturn(Mono.defer(() -> + Mono.error(new R2dbcTransientResourceException("Rollback should also fail")))); + + TransactionalOperator operator = TransactionalOperator.create(tm); + + ConnectionFactoryUtils.getConnection(connectionFactoryMock) + .doOnNext(connection -> connection.createStatement("foo")).then() + .as(operator::transactional) + .as(StepVerifier::create) + .verifyError(TransientDataAccessResourceException.class); + + verify(connectionMock).isAutoCommit(); + verify(connectionMock).beginTransaction(any(io.r2dbc.spi.TransactionDefinition.class)); + verify(connectionMock).createStatement("foo"); + verify(connectionMock).commitTransaction(); + verify(connectionMock).rollbackTransaction(); + verify(connectionMock).close(); + verifyNoMoreInteractions(connectionMock); + } + @Test void testTransactionSetRollbackOnly() { when(connectionMock.isAutoCommit()).thenReturn(false); diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/SingleConnectionFactoryTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/SingleConnectionFactoryTests.java index a445abe3ce2a..773e01ce5330 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/SingleConnectionFactoryTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/SingleConnectionFactoryTests.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,10 +27,10 @@ import reactor.test.StepVerifier; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.never; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Tests for {@link SingleConnectionFactory}. diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyTests.java index fec9fbe626db..751b59db2a37 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/TransactionAwareConnectionFactoryProxyTests.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,11 +31,11 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalStateException; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.times; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.verifyNoInteractions; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.when; /** * Tests for {@link TransactionAwareConnectionFactoryProxy}. diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/CompositeDatabasePopulatorTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/CompositeDatabasePopulatorTests.java index 3bd7f268082f..d5ffd97179d0 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/CompositeDatabasePopulatorTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/CompositeDatabasePopulatorTests.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,10 +25,10 @@ import reactor.core.publisher.Mono; import reactor.test.StepVerifier; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.times; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Tests for {@link CompositeDatabasePopulator}. diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializerTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializerTests.java index 5e79730624ad..90dded294894 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializerTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ConnectionFactoryInitializerTests.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,8 +24,8 @@ import reactor.core.publisher.Mono; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link ConnectionFactoryInitializer}. diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulatorTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulatorTests.java index eaf39d60c87c..bd8b7f03e337 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulatorTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/init/ResourceDatabasePopulatorTests.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,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; -import static org.mockito.BDDMockito.mock; +import static org.mockito.Mockito.mock; /** * Tests for {@link ResourceDatabasePopulator}. diff --git a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookupTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookupTests.java index 9674fd77d2e2..389ed6f81f2e 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookupTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/connection/lookup/BeanFactoryConnectionFactoryLookupTests.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,8 +28,8 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; /** * Tests for {@link BeanFactoryConnectionFactoryLookup}. 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 19332ecd4033..c992aa242f9b 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.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 @@ * @author Mark Paluch * @author Mingyuan Wu * @author Juergen Hoeller + * @author Sam Brannen */ abstract class AbstractDatabaseClientIntegrationTests { @@ -121,7 +123,8 @@ void executeInsertWithMap() { 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 @@ void executeDeferred() { 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 @@ void shouldEmitGeneratedKey() { } + @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/DefaultDatabaseClientTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientTests.java index 93f9a65d97fb..64ffa9073e6c 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/DefaultDatabaseClientTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,6 +29,7 @@ import io.r2dbc.spi.test.MockResult; import io.r2dbc.spi.test.MockRow; import io.r2dbc.spi.test.MockRowMetadata; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InOrder; @@ -43,21 +44,20 @@ import reactor.test.StepVerifier; import org.springframework.dao.IncorrectResultSizeDataAccessException; -import org.springframework.lang.Nullable; import org.springframework.r2dbc.core.binding.BindMarkersFactory; import org.springframework.r2dbc.core.binding.BindTarget; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.BDDMockito.doReturn; -import static org.mockito.BDDMockito.inOrder; -import static org.mockito.BDDMockito.mock; -import static org.mockito.BDDMockito.times; -import static org.mockito.BDDMockito.verify; -import static org.mockito.BDDMockito.verifyNoInteractions; -import static org.mockito.BDDMockito.verifyNoMoreInteractions; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; /** * Tests for {@link DefaultDatabaseClient}. @@ -488,7 +488,7 @@ private MockResult mockSingleColumnEmptyResult() { * Mocks a {@link Result} with a single column "name" and a single row if a non-null * row is provided. */ - private MockResult mockSingleColumnResult(@Nullable MockRow.Builder row) { + private MockResult mockSingleColumnResult(MockRow.@Nullable Builder row) { MockResult.Builder resultBuilder = MockResult.builder(); if (row != null) { MockRowMetadata metadata = MockRowMetadata.builder().columnMetadata( 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 6d23d4810371..725bcf72a924 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.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 @@ * @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 @@ void substituteNamedParameters() { 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 @@ void substituteNamedParameters() { 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 @@ void substituteObjectArray() { 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 @@ void parseSqlStatementWithPostgresCasting() { 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 @@ void shouldAllowParsingMultipleUseOfParameter() { void multipleEqualParameterReferencesBindsValueOnce() { String sql = "SELECT * FROM person where name = :id or lastname = :id"; - BindMarkersFactory factory = BindMarkersFactory.indexed("$", 0); - - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource( - Collections.singletonMap("id", Parameters.in("foo")))); - - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name = $0 or lastname = $0"); - - 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(); - } - }); + MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo"))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source); + + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name = $1 or lastname = $1"); + + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + 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); - - MultiValueMap bindings = new LinkedMultiValueMap<>(); - - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource(Collections.singletonMap("ids", - Parameters.in(Arrays.asList("foo", "bar", "baz"))))); - - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name IN ($0, $1, $2) or lastname IN ($0, $1, $2)"); - - 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(bindings).containsEntry(0, Collections.singletonList("foo")) // - .containsEntry(1, Collections.singletonList("bar")) // - .containsEntry(2, Collections.singletonList("baz")); + MapBindParameterSource source = new MapBindParameterSource(Map.of("ids", + Parameters.in(List.of("foo", "bar", "baz")))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source); + + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name IN ($1, $2, $3) or lastname IN ($1, $2, $3)"); + + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + assertThat(trackingBindTarget.bindings) + .hasSize(3) + .containsEntry(0, "foo") + .containsEntry(1, "bar") + .containsEntry(2, "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("?"); - - 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 = ?"); - - Map bindValues = new LinkedHashMap<>(); - - 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")); + MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in("foo"))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, ANONYMOUS_MARKERS, source); + + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name = ? or lastname = ?"); + + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + operation.bindTo(trackingBindTarget); + + 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); - - PreparedOperation operation = NamedParameterUtils.substituteNamedParameters( - sql, factory, new MapBindParameterSource( - Collections.singletonMap("id", Parameters.in(String.class)))); - - assertThat(operation.toQuery()).isEqualTo( - "SELECT * FROM person where name = $0 or lastname = $0"); - - 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(); - } - }); + MapBindParameterSource source = new MapBindParameterSource(Map.of("id", Parameters.in(String.class))); + PreparedOperation operation = NamedParameterUtils.substituteNamedParameters(sql, INDEXED_MARKERS, source); + + assertThat(operation.toQuery()) + .isEqualTo("SELECT * FROM person where name = $1 or lastname = $1"); + + TrackingBindTarget trackingBindTarget = new TrackingBindTarget(); + + 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-r2dbc/src/test/java/org/springframework/r2dbc/core/R2dbcBeanPropertyRowMapperTests.java b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/R2dbcBeanPropertyRowMapperTests.java index 192b9f655b1b..e718d0cdf1c4 100644 --- a/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/R2dbcBeanPropertyRowMapperTests.java +++ b/spring-r2dbc/src/test/java/org/springframework/r2dbc/core/R2dbcBeanPropertyRowMapperTests.java @@ -96,8 +96,8 @@ void rowTypeAndMappingTypeMisaligned() { assertThatExceptionOfType(TypeMismatchException.class) .isThrownBy(() -> mapper.apply(EXTENDED_PERSON_ROW)) - .withMessage("Failed to convert property value of type 'java.lang.String' to required type " - + "'java.lang.String' for property 'address'; simulating type mismatch for address"); + .withMessage("Failed to convert property value of type 'java.lang.String' to required type " + + "'java.lang.String' for property 'address'; simulating type mismatch for address"); } @ParameterizedTest diff --git a/spring-test/spring-test.gradle b/spring-test/spring-test.gradle index f0bc5a7f635f..4742c723567f 100644 --- a/spring-test/spring-test.gradle +++ b/spring-test/spring-test.gradle @@ -27,7 +27,6 @@ dependencies { optional("jakarta.websocket:jakarta.websocket-api") optional("jakarta.websocket:jakarta.websocket-client-api") optional("jakarta.xml.bind:jakarta.xml.bind-api") - optional("javax.inject:javax.inject") optional("junit:junit") optional("org.apache.groovy:groovy") optional("org.apache.tomcat.embed:tomcat-embed-core") @@ -78,10 +77,11 @@ dependencies { } testImplementation("org.awaitility:awaitility") testImplementation("org.easymock:easymock") - testImplementation("org.hibernate:hibernate-core-jakarta") - testImplementation("org.hibernate:hibernate-validator") + testImplementation("org.hibernate.orm:hibernate-core") + 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") @@ -105,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 88072db943d3..6af12761e22a 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 @@ * @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 MockEnvironment() { /** * 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 3ef180fcf22b..8b2c6e1d7767 100644 --- a/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java +++ b/spring-test/src/main/java/org/springframework/mock/env/MockPropertySource.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,7 +26,7 @@ * a user-provided {@link Properties} object, or if omitted during construction, * the implementation will initialize its own. * - * The {@link #setProperty} and {@link #withProperty} methods are exposed for + *

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

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

    Useful for method chaining and fluent-style use. * @return this {@link MockPropertySource} instance */ public MockPropertySource withProperty(String name, Object value) { diff --git a/spring-test/src/main/java/org/springframework/mock/env/package-info.java b/spring-test/src/main/java/org/springframework/mock/env/package-info.java index 964c7688bc10..0fa7e3a27a97 100644 --- a/spring-test/src/main/java/org/springframework/mock/env/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/env/package-info.java @@ -7,9 +7,7 @@ *

    These mocks are useful for developing out-of-container * unit tests for code that depends on environment-specific properties. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.env; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 10736fe4f024..b56b4ea9cbcc 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. @@ -21,10 +21,11 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.mock.http.MockHttpOutputMessage; import org.springframework.util.Assert; import org.springframework.web.util.UriComponentsBuilder; @@ -43,13 +44,11 @@ public class MockClientHttpRequest extends MockHttpOutputMessage implements Clie private URI uri; - @Nullable - private ClientHttpResponse clientHttpResponse; + private @Nullable ClientHttpResponse clientHttpResponse; private boolean executed = false; - @Nullable - Map attributes; + @Nullable Map attributes; /** @@ -105,7 +104,7 @@ public URI getURI() { /** * 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/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java index 3feddab58699..8809f4d60825 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/package-info.java @@ -3,9 +3,7 @@ * This package contains {@code MockClientHttpRequest} and * {@code MockClientHttpResponse}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.http.client; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/package-info.java index 36f5a5bb3950..1bba82406207 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/client/reactive/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/http/client/reactive/package-info.java @@ -1,9 +1,7 @@ /** * Mock implementations of reactive HTTP client contracts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.http.client.reactive; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/mock/http/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/package-info.java index 18bda66888a6..df2c8eaefec8 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/http/package-info.java @@ -3,9 +3,7 @@ * This package contains {@code MockHttpInputMessage} and * {@code MockHttpOutputMessage}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.http; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java index 1b9d39b50b60..d28816d52b30 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/MockServerHttpRequest.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,6 +26,7 @@ import java.util.Locale; import java.util.Optional; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -38,7 +39,6 @@ import org.springframework.http.MediaType; import org.springframework.http.server.reactive.AbstractServerHttpRequest; import org.springframework.http.server.reactive.SslInfo; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; @@ -56,14 +56,11 @@ public final class MockServerHttpRequest extends AbstractServerHttpRequest { private final MultiValueMap cookies; - @Nullable - private final InetSocketAddress localAddress; + private final @Nullable InetSocketAddress localAddress; - @Nullable - private final InetSocketAddress remoteAddress; + private final @Nullable InetSocketAddress remoteAddress; - @Nullable - private final SslInfo sslInfo; + private final @Nullable SslInfo sslInfo; private final Flux body; @@ -82,20 +79,17 @@ private MockServerHttpRequest(HttpMethod httpMethod, @Override - @Nullable - public InetSocketAddress getLocalAddress() { + public @Nullable InetSocketAddress getLocalAddress() { return this.localAddress; } @Override - @Nullable - public InetSocketAddress getRemoteAddress() { + public @Nullable InetSocketAddress getRemoteAddress() { return this.remoteAddress; } @Override - @Nullable - protected SslInfo initSslInfo() { + protected @Nullable SslInfo initSslInfo() { return this.sslInfo; } @@ -125,7 +119,7 @@ public T getNativeRequest() { * @param uriVars zero or more URI variables * @return the created builder */ - public static BaseBuilder get(String urlTemplate, Object... uriVars) { + public static BaseBuilder get(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.GET, urlTemplate, uriVars); } @@ -135,7 +129,7 @@ public static BaseBuilder get(String urlTemplate, Object... uriVars) { * @param uriVars zero or more URI variables * @return the created builder */ - public static BaseBuilder head(String urlTemplate, Object... uriVars) { + public static BaseBuilder head(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.HEAD, urlTemplate, uriVars); } @@ -145,7 +139,7 @@ public static BaseBuilder head(String urlTemplate, Object... uriVars) { * @param uriVars zero or more URI variables * @return the created builder */ - public static BodyBuilder post(String urlTemplate, Object... uriVars) { + public static BodyBuilder post(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.POST, urlTemplate, uriVars); } @@ -156,7 +150,7 @@ public static BodyBuilder post(String urlTemplate, Object... uriVars) { * @param uriVars zero or more URI variables * @return the created builder */ - public static BodyBuilder put(String urlTemplate, Object... uriVars) { + public static BodyBuilder put(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.PUT, urlTemplate, uriVars); } @@ -166,7 +160,7 @@ public static BodyBuilder put(String urlTemplate, Object... uriVars) { * @param uriVars zero or more URI variables * @return the created builder */ - public static BodyBuilder patch(String urlTemplate, Object... uriVars) { + public static BodyBuilder patch(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.PATCH, urlTemplate, uriVars); } @@ -176,7 +170,7 @@ public static BodyBuilder patch(String urlTemplate, Object... uriVars) { * @param uriVars zero or more URI variables * @return the created builder */ - public static BaseBuilder delete(String urlTemplate, Object... uriVars) { + public static BaseBuilder delete(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.DELETE, urlTemplate, uriVars); } @@ -186,7 +180,7 @@ public static BaseBuilder delete(String urlTemplate, Object... uriVars) { * @param uriVars zero or more URI variables * @return the created builder */ - public static BaseBuilder options(String urlTemplate, Object... uriVars) { + public static BaseBuilder options(String urlTemplate, @Nullable Object... uriVars) { return method(HttpMethod.OPTIONS, urlTemplate, uriVars); } @@ -211,27 +205,11 @@ public static BodyBuilder method(HttpMethod method, URI url) { * @param vars variables to expand into the template * @return the created builder */ - public static BodyBuilder method(HttpMethod method, String uri, Object... vars) { + public static BodyBuilder method(HttpMethod method, String uri, @Nullable Object... vars) { return method(method, toUri(uri, vars)); } - /** - * Create a builder with a raw HTTP method value that is outside the - * range of {@link HttpMethod} enum values. - * @param httpMethod the HTTP methodValue value - * @param uri the URI template for target the URL - * @param vars variables to expand into the template - * @return the created builder - * @since 5.2.7 - * @deprecated as of Spring Framework 6.0 in favor of {@link #method(HttpMethod, String, Object...)} - */ - @Deprecated(since = "6.0") - public static BodyBuilder method(String httpMethod, String uri, Object... vars) { - Assert.hasText(httpMethod, "HTTP method is required."); - return new DefaultBodyBuilder(HttpMethod.valueOf(httpMethod), toUri(uri, vars)); - } - - private static URI toUri(String uri, Object[] vars) { + private static URI toUri(String uri, @Nullable Object[] vars) { return UriComponentsBuilder.fromUriString(uri).buildAndExpand(vars).encode().toUri(); } @@ -305,7 +283,7 @@ public interface BaseBuilder> { * Add the given header values. * @param headers the header values */ - B headers(MultiValueMap headers); + B headers(HttpHeaders headers); /** * Set the list of acceptable {@linkplain MediaType media types}, as @@ -415,8 +393,7 @@ private static class DefaultBodyBuilder implements BodyBuilder { private final URI url; - @Nullable - private String contextPath; + private @Nullable String contextPath; private final UriComponentsBuilder queryParamsBuilder = UriComponentsBuilder.newInstance(); @@ -424,14 +401,11 @@ private static class DefaultBodyBuilder implements BodyBuilder { private final MultiValueMap cookies = new LinkedMultiValueMap<>(); - @Nullable - private InetSocketAddress remoteAddress; + private @Nullable InetSocketAddress remoteAddress; - @Nullable - private InetSocketAddress localAddress; + private @Nullable InetSocketAddress localAddress; - @Nullable - private SslInfo sslInfo; + private @Nullable SslInfo sslInfo; DefaultBodyBuilder(HttpMethod method, URI url) { this.method = method; @@ -494,7 +468,7 @@ public BodyBuilder header(String headerName, String... headerValues) { } @Override - public BodyBuilder headers(MultiValueMap headers) { + public BodyBuilder headers(HttpHeaders headers) { this.headers.putAll(headers); return this; } diff --git a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/package-info.java b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/package-info.java index b989b2d96b71..599c9d115fbf 100644 --- a/spring-test/src/main/java/org/springframework/mock/http/server/reactive/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/http/server/reactive/package-info.java @@ -1,9 +1,7 @@ /** * Mock implementations of reactive HTTP server contracts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.http.server.reactive; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java b/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java index a0a739765e36..25249a7d93cb 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java +++ b/spring-test/src/main/java/org/springframework/mock/web/HeaderValueHolder.java @@ -21,7 +21,8 @@ import java.util.LinkedList; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.CollectionUtils; /** @@ -63,13 +64,11 @@ List getStringValues() { return this.values.stream().map(Object::toString).toList(); } - @Nullable - Object getValue() { + @Nullable Object getValue() { return (!this.values.isEmpty() ? this.values.get(0) : null); } - @Nullable - String getStringValue() { + @Nullable String getStringValue() { return (!this.values.isEmpty() ? String.valueOf(this.values.get(0)) : null); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockAsyncContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockAsyncContext.java index 982d4e3b261e..20761a496e81 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockAsyncContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockAsyncContext.java @@ -29,9 +29,9 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.util.WebUtils; @@ -45,13 +45,11 @@ public class MockAsyncContext implements AsyncContext { private final HttpServletRequest request; - @Nullable - private final HttpServletResponse response; + private final @Nullable HttpServletResponse response; private final List listeners = new ArrayList<>(); - @Nullable - private String dispatchedPath; + private @Nullable String dispatchedPath; private long timeout = 10 * 1000L; @@ -82,8 +80,7 @@ public ServletRequest getRequest() { } @Override - @Nullable - public ServletResponse getResponse() { + public @Nullable ServletResponse getResponse() { return this.response; } @@ -110,8 +107,7 @@ public void dispatch(@Nullable ServletContext context, String path) { } } - @Nullable - public String getDispatchedPath() { + public @Nullable String getDispatchedPath() { return this.dispatchedPath; } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java b/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java index 81a905150eca..6bc5923cd85e 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockBodyContent.java @@ -24,8 +24,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.jsp.JspWriter; import jakarta.servlet.jsp.tagext.BodyContent; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Mock implementation of the {@link jakarta.servlet.jsp.tagext.BodyContent} class. diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java index 9347e82173dc..39a70aeb8f15 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockCookie.java @@ -21,9 +21,9 @@ import java.time.format.DateTimeFormatter; import jakarta.servlet.http.Cookie; +import org.jspecify.annotations.Nullable; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -54,8 +54,7 @@ public class MockCookie extends Cookie { private static final String EXPIRES = "Expires"; - @Nullable - private ZonedDateTime expires; + private @Nullable ZonedDateTime expires; /** @@ -81,8 +80,7 @@ public void setExpires(@Nullable ZonedDateTime expires) { * @return the "Expires" attribute for this cookie, or {@code null} if not set * @since 5.1.11 */ - @Nullable - public ZonedDateTime getExpires() { + public @Nullable ZonedDateTime getExpires() { return this.expires; } @@ -101,8 +99,7 @@ public void setSameSite(@Nullable String sameSite) { * Get the "SameSite" attribute for this cookie. * @return the "SameSite" attribute for this cookie, or {@code null} if not set */ - @Nullable - public String getSameSite() { + public @Nullable String getSameSite() { return getAttribute(SAME_SITE); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java index 2a48a42e5edf..3d666d56d0bd 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterChain.java @@ -28,8 +28,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; @@ -50,16 +50,13 @@ */ public class MockFilterChain implements FilterChain { - @Nullable - private ServletRequest request; + private @Nullable ServletRequest request; - @Nullable - private ServletResponse response; + private @Nullable ServletResponse response; private final List filters; - @Nullable - private Iterator iterator; + private @Nullable Iterator iterator; /** @@ -100,16 +97,14 @@ private static List initFilterList(Servlet servlet, Filter... filters) { /** * Return the request that {@link #doFilter} has been called with. */ - @Nullable - public ServletRequest getRequest() { + public @Nullable ServletRequest getRequest() { return this.request; } /** * Return the response that {@link #doFilter} has been called with. */ - @Nullable - public ServletResponse getResponse() { + public @Nullable ServletResponse getResponse() { return this.response; } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java index 5557276184f0..c6904bce4df2 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterConfig.java @@ -23,8 +23,8 @@ import jakarta.servlet.FilterConfig; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -97,8 +97,7 @@ public void addInitParameter(String name, String value) { } @Override - @Nullable - public String getInitParameter(String name) { + public @Nullable String getInitParameter(String name) { Assert.notNull(name, "Parameter name must not be null"); return this.initParameters.get(name); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockFilterRegistration.java b/spring-test/src/main/java/org/springframework/mock/web/MockFilterRegistration.java index b73c4bfb4dc3..3edbe35746ec 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockFilterRegistration.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockFilterRegistration.java @@ -29,8 +29,7 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.FilterRegistration; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Mock implementation of {@link FilterRegistration}. @@ -68,9 +67,8 @@ public String getName() { return this.name; } - @Nullable @Override - public String getClassName() { + public @Nullable String getClassName() { return this.className; } @@ -79,9 +77,8 @@ public boolean setInitParameter(String name, String value) { return (this.initParameters.putIfAbsent(name, value) != null); } - @Nullable @Override - public String getInitParameter(String name) { + public @Nullable String getInitParameter(String name) { return this.initParameters.get(name); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletMapping.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletMapping.java index 17615a3bdcf9..50d854c52031 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletMapping.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletMapping.java @@ -18,8 +18,7 @@ import jakarta.servlet.http.HttpServletMapping; import jakarta.servlet.http.MappingMatch; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Mock implementation of {@link HttpServletMapping}. @@ -39,8 +38,7 @@ public class MockHttpServletMapping implements HttpServletMapping { private final String servletName; - @Nullable - private final MappingMatch mappingMatch; + private final @Nullable MappingMatch mappingMatch; public MockHttpServletMapping( @@ -69,8 +67,7 @@ public String getServletName() { } @Override - @Nullable - public MappingMatch getMappingMatch() { + public @Nullable MappingMatch getMappingMatch() { return this.mappingMatch; } 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 bb1b861fdd64..35ee7c332658 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. @@ -62,10 +62,10 @@ import jakarta.servlet.http.HttpUpgradeHandler; import jakarta.servlet.http.MappingMatch; import jakarta.servlet.http.Part; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.LinkedMultiValueMap; @@ -170,20 +170,15 @@ public class MockHttpServletRequest implements HttpServletRequest { private final Map attributes = new LinkedHashMap<>(); - @Nullable - private String characterEncoding; + private @Nullable String characterEncoding; - @Nullable - private byte[] content; + private byte @Nullable [] content; - @Nullable - private String contentType; + private @Nullable String contentType; - @Nullable - private ServletInputStream inputStream; + private @Nullable ServletInputStream inputStream; - @Nullable - private BufferedReader reader; + private @Nullable BufferedReader reader; private final Map parameters = new LinkedHashMap<>(16); @@ -216,8 +211,7 @@ public class MockHttpServletRequest implements HttpServletRequest { private boolean asyncSupported = false; - @Nullable - private MockAsyncContext asyncContext; + private @Nullable MockAsyncContext asyncContext; private DispatcherType dispatcherType = DispatcherType.REQUEST; @@ -226,46 +220,35 @@ public class MockHttpServletRequest implements HttpServletRequest { // HttpServletRequest properties // --------------------------------------------------------------------- - @Nullable - private String authType; + private @Nullable String authType; - @Nullable - private Cookie[] cookies; + private Cookie @Nullable [] cookies; private final Map headers = new LinkedCaseInsensitiveMap<>(); - @Nullable - private String method; + private @Nullable String method; - @Nullable - private String pathInfo; + private @Nullable String pathInfo; private String contextPath = ""; - @Nullable - private String queryString; + private @Nullable String queryString; - @Nullable - private String remoteUser; + private @Nullable String remoteUser; private final Set userRoles = new HashSet<>(); - @Nullable - private Principal userPrincipal; + private @Nullable Principal userPrincipal; - @Nullable - private String requestedSessionId; + private @Nullable String requestedSessionId; - @Nullable - private String uriTemplate; + private @Nullable String uriTemplate; - @Nullable - private String requestURI; + private @Nullable String requestURI; private String servletPath = ""; - @Nullable - private HttpSession session; + private @Nullable HttpSession session; private boolean requestedSessionIdValid = true; @@ -275,8 +258,7 @@ public class MockHttpServletRequest implements HttpServletRequest { private final MultiValueMap parts = new LinkedMultiValueMap<>(); - @Nullable - private HttpServletMapping httpServletMapping; + private @Nullable HttpServletMapping httpServletMapping; // --------------------------------------------------------------------- @@ -385,8 +367,7 @@ protected void checkActive() throws IllegalStateException { // --------------------------------------------------------------------- @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { checkActive(); return this.attributes.get(name); } @@ -398,8 +379,7 @@ public Enumeration getAttributeNames() { } @Override - @Nullable - public String getCharacterEncoding() { + public @Nullable String getCharacterEncoding() { return this.characterEncoding; } @@ -429,7 +409,7 @@ private void updateContentTypeHeader() { * @see #getContentAsByteArray() * @see #getContentAsString() */ - public void setContent(@Nullable byte[] content) { + public void setContent(byte @Nullable [] content) { this.content = content; this.inputStream = null; this.reader = null; @@ -442,8 +422,7 @@ public void setContent(@Nullable byte[] content) { * @see #setContent(byte[]) * @see #getContentAsString() */ - @Nullable - public byte[] getContentAsByteArray() { + public byte @Nullable [] getContentAsByteArray() { return this.content; } @@ -458,8 +437,7 @@ public byte[] getContentAsByteArray() { * @see #setCharacterEncoding(String) * @see #getContentAsByteArray() */ - @Nullable - public String getContentAsString() throws IllegalStateException, UnsupportedEncodingException { + public @Nullable String getContentAsString() throws IllegalStateException, UnsupportedEncodingException { Assert.state(this.characterEncoding != null, "Cannot get content as a String for a null character encoding. " + "Consider setting the characterEncoding in the request."); @@ -502,8 +480,7 @@ public void setContentType(@Nullable String contentType) { } @Override - @Nullable - public String getContentType() { + public @Nullable String getContentType() { return this.contentType; } @@ -628,8 +605,7 @@ public void removeAllParameters() { } @Override - @Nullable - public String getParameter(String name) { + public @Nullable String getParameter(String name) { Assert.notNull(name, "Parameter name must not be null"); String[] arr = this.parameters.get(name); return (arr != null && arr.length > 0 ? arr[0] : null); @@ -641,8 +617,7 @@ public Enumeration getParameterNames() { } @Override - @Nullable - public String[] getParameterValues(String name) { + public String @Nullable [] getParameterValues(String name) { Assert.notNull(name, "Parameter name must not be null"); return this.parameters.get(name); } @@ -965,8 +940,7 @@ public void setAsyncContext(@Nullable MockAsyncContext asyncContext) { } @Override - @Nullable - public AsyncContext getAsyncContext() { + public @Nullable AsyncContext getAsyncContext() { return this.asyncContext; } @@ -1021,12 +995,11 @@ public void setAuthType(@Nullable String authType) { } @Override - @Nullable - public String getAuthType() { + public @Nullable String getAuthType() { return this.authType; } - public void setCookies(@Nullable Cookie... cookies) { + public void setCookies(Cookie @Nullable ... cookies) { this.cookies = (ObjectUtils.isEmpty(cookies) ? null : cookies); if (this.cookies == null) { removeHeader(HttpHeaders.COOKIE); @@ -1043,8 +1016,7 @@ private static String encodeCookies(Cookie... cookies) { } @Override - @Nullable - public Cookie[] getCookies() { + public Cookie @Nullable [] getCookies() { return this.cookies; } @@ -1064,8 +1036,7 @@ public Cookie[] getCookies() { * @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) && @@ -1166,8 +1137,7 @@ private long parseDateHeader(String name, String value) { } @Override - @Nullable - public String getHeader(String name) { + public @Nullable String getHeader(String name) { HeaderValueHolder header = this.headers.get(name); return (header != null ? header.getStringValue() : null); } @@ -1207,8 +1177,7 @@ public void setMethod(@Nullable String method) { } @Override - @Nullable - public String getMethod() { + public @Nullable String getMethod() { return this.method; } @@ -1217,14 +1186,12 @@ public void setPathInfo(@Nullable String pathInfo) { } @Override - @Nullable - public String getPathInfo() { + public @Nullable String getPathInfo() { return this.pathInfo; } @Override - @Nullable - public String getPathTranslated() { + public @Nullable String getPathTranslated() { return (this.pathInfo != null ? this.servletContext.getRealPath(this.pathInfo) : null); } @@ -1242,8 +1209,7 @@ public void setQueryString(@Nullable String queryString) { } @Override - @Nullable - public String getQueryString() { + public @Nullable String getQueryString() { return this.queryString; } @@ -1252,8 +1218,7 @@ public void setRemoteUser(@Nullable String remoteUser) { } @Override - @Nullable - public String getRemoteUser() { + public @Nullable String getRemoteUser() { return this.remoteUser; } @@ -1273,8 +1238,7 @@ public void setUserPrincipal(@Nullable Principal userPrincipal) { } @Override - @Nullable - public Principal getUserPrincipal() { + public @Nullable Principal getUserPrincipal() { return this.userPrincipal; } @@ -1283,8 +1247,7 @@ public void setRequestedSessionId(@Nullable String requestedSessionId) { } @Override - @Nullable - public String getRequestedSessionId() { + public @Nullable String getRequestedSessionId() { return this.requestedSessionId; } @@ -1301,8 +1264,7 @@ public void setUriTemplate(@Nullable String uriTemplate) { * Return the original URI template used to prepare the request, if any. * @since 6.2 */ - @Nullable - public String getUriTemplate() { + public @Nullable String getUriTemplate() { return this.uriTemplate; } @@ -1311,8 +1273,7 @@ public void setRequestURI(@Nullable String requestURI) { } @Override - @Nullable - public String getRequestURI() { + public @Nullable String getRequestURI() { return this.requestURI; } @@ -1351,8 +1312,7 @@ public void setSession(HttpSession session) { } @Override - @Nullable - public HttpSession getSession(boolean create) { + public @Nullable HttpSession getSession(boolean create) { checkActive(); // Reset session if invalidated. if (this.session instanceof MockHttpSession mockSession && mockSession.isInvalid()) { @@ -1366,8 +1326,7 @@ public HttpSession getSession(boolean create) { } @Override - @Nullable - public HttpSession getSession() { + public @Nullable HttpSession getSession() { return getSession(true); } @@ -1435,8 +1394,7 @@ public void addPart(Part part) { } @Override - @Nullable - public Part getPart(String name) throws IOException, ServletException { + public @Nullable Part getPart(String name) throws IOException, ServletException { return this.parts.getFirst(name); } @@ -1466,8 +1424,7 @@ public HttpServletMapping getHttpServletMapping() { * This helps {@link org.springframework.web.util.ServletRequestPathUtils} * to take into account the Servlet path when parsing the requestURI. */ - @Nullable - private MappingMatch determineMappingMatch() { + private @Nullable MappingMatch determineMappingMatch() { if (StringUtils.hasText(this.requestURI) && StringUtils.hasText(this.servletPath)) { String path = UrlPathHelper.defaultInstance.getRequestUri(this); String prefix = this.contextPath + this.servletPath; diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 3217d6e8eb15..95bc2551b8e1 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -43,10 +43,10 @@ import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedCaseInsensitiveMap; import org.springframework.util.StringUtils; @@ -97,15 +97,13 @@ public class MockHttpServletResponse implements HttpServletResponse { private final ByteArrayOutputStream content = new ByteArrayOutputStream(1024); - private final ServletOutputStream outputStream = new ResponseServletOutputStream(this.content); + private @Nullable ServletOutputStream outputStream; - @Nullable - private PrintWriter writer; + private @Nullable PrintWriter writer; private long contentLength = 0; - @Nullable - private String contentType; + private @Nullable String contentType; private int bufferSize = 4096; @@ -124,16 +122,14 @@ public class MockHttpServletResponse implements HttpServletResponse { private int status = HttpServletResponse.SC_OK; - @Nullable - private String errorMessage; + private @Nullable String errorMessage; //--------------------------------------------------------------------- // Properties for MockRequestDispatcher //--------------------------------------------------------------------- - @Nullable - private String forwardedUrl; + private @Nullable String forwardedUrl; private final List includedUrls = new ArrayList<>(); @@ -262,12 +258,17 @@ public String getCharacterEncoding() { @Override public ServletOutputStream getOutputStream() { Assert.state(this.outputStreamAccessAllowed, "OutputStream access not allowed"); + Assert.state(this.writer == null, "getWriter() has already been called"); + if (this.outputStream == null) { + this.outputStream = new ResponseServletOutputStream(this.content); + } return this.outputStream; } @Override public PrintWriter getWriter() throws UnsupportedEncodingException { Assert.state(this.writerAccessAllowed, "Writer access not allowed"); + Assert.state(this.outputStream == null, "getOutputStream() has already been called"); if (this.writer == null) { Writer targetWriter = new OutputStreamWriter(this.content, getCharacterEncoding()); this.writer = new ResponsePrintWriter(targetWriter); @@ -319,8 +320,10 @@ public String getContentAsString(Charset fallbackCharset) throws UnsupportedEnco @Override public void setContentLength(int contentLength) { - this.contentLength = contentLength; - doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); + if (!this.committed) { + this.contentLength = contentLength; + doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); + } } /** @@ -334,8 +337,10 @@ public int getContentLength() { @Override public void setContentLengthLong(long contentLength) { - this.contentLength = contentLength; - doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); + if (!this.committed) { + this.contentLength = contentLength; + doAddHeaderValue(HttpHeaders.CONTENT_LENGTH, contentLength, true); + } } public long getContentLengthLong() { @@ -365,11 +370,13 @@ else if (mediaType.isCompatibleWith(MediaType.APPLICATION_JSON) || } updateContentTypePropertyAndHeader(); } + else { + this.headers.remove(HttpHeaders.CONTENT_TYPE); + } } @Override - @Nullable - public String getContentType() { + public @Nullable String getContentType() { return this.contentType; } @@ -422,6 +429,8 @@ public void reset() { this.headers.clear(); this.status = HttpServletResponse.SC_OK; this.errorMessage = null; + this.writer = null; + this.outputStream = null; } @Override @@ -506,8 +515,7 @@ public Cookie[] getCookies() { return this.cookies.toArray(new Cookie[0]); } - @Nullable - public Cookie getCookie(String name) { + public @Nullable Cookie getCookie(String name) { Assert.notNull(name, "Cookie name must not be null"); for (Cookie cookie : this.cookies) { if (name.equals(cookie.getName())) { @@ -542,8 +550,7 @@ public Collection getHeaderNames() { * @see HttpServletResponse#getHeader(String) */ @Override - @Nullable - public String getHeader(String name) { + public @Nullable String getHeader(String name) { HeaderValueHolder header = this.headers.get(name); return (header != null ? header.getStringValue() : null); } @@ -573,8 +580,7 @@ public List getHeaders(String name) { * @param name the name of the header * @return the associated header value, or {@code null} if none */ - @Nullable - public Object getHeaderValue(String name) { + public @Nullable Object getHeaderValue(String name) { HeaderValueHolder header = this.headers.get(name); return (header != null ? header.getValue() : null); } @@ -645,8 +651,7 @@ public void sendRedirect(String url, int sc, boolean clearBuffer) throws IOExcep setCommitted(true); } - @Nullable - public String getRedirectedUrl() { + public @Nullable String getRedirectedUrl() { return getHeader(HttpHeaders.LOCATION); } @@ -685,17 +690,12 @@ private DateFormat newDateFormat() { } @Override - public void setHeader(String name, @Nullable String value) { - if (value == null) { - this.headers.remove(name); - } - else { - setHeaderValue(name, value); - } + public void setHeader(@Nullable String name, @Nullable String value) { + setHeaderValue(name, value); } @Override - public void addHeader(String name, @Nullable String value) { + public void addHeader(@Nullable String name, @Nullable String value) { addHeaderValue(name, value); } @@ -709,8 +709,8 @@ public void addIntHeader(String name, int value) { addHeaderValue(name, value); } - private void setHeaderValue(String name, @Nullable Object value) { - if (value == null) { + private void setHeaderValue(@Nullable String name, @Nullable Object value) { + if (name == null) { return; } boolean replaceHeader = true; @@ -720,8 +720,8 @@ private void setHeaderValue(String name, @Nullable Object value) { doAddHeaderValue(name, value, replaceHeader); } - private void addHeaderValue(String name, @Nullable Object value) { - if (value == null) { + private void addHeaderValue(@Nullable String name, @Nullable Object value) { + if (name == null) { return; } boolean replaceHeader = false; @@ -731,7 +731,19 @@ private void addHeaderValue(String name, @Nullable Object value) { doAddHeaderValue(name, value, replaceHeader); } - private boolean setSpecialHeader(String name, Object value, boolean replaceHeader) { + private boolean setSpecialHeader(String name, @Nullable Object value, boolean replaceHeader) { + if (value == null) { + if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { + setContentType(null); + } + else if (HttpHeaders.CONTENT_LENGTH.equalsIgnoreCase(name)) { + this.contentLength = 0; + } + else { + this.headers.remove(name); + } + return true; + } if (HttpHeaders.CONTENT_TYPE.equalsIgnoreCase(name)) { setContentType(value.toString()); return true; @@ -771,7 +783,7 @@ else if (HttpHeaders.SET_COOKIE.equalsIgnoreCase(name)) { } } - private void doAddHeaderValue(String name, Object value, boolean replace) { + private void doAddHeaderValue(String name, @Nullable Object value, boolean replace) { Assert.notNull(value, "Header value must not be null"); HeaderValueHolder header = this.headers.computeIfAbsent(name, key -> new HeaderValueHolder()); if (replace) { @@ -811,8 +823,7 @@ public int getStatus() { /** * Return the error message used when calling {@link HttpServletResponse#sendError(int, String)}. */ - @Nullable - public String getErrorMessage() { + public @Nullable String getErrorMessage() { return this.errorMessage; } @@ -825,8 +836,7 @@ public void setForwardedUrl(@Nullable String forwardedUrl) { this.forwardedUrl = forwardedUrl; } - @Nullable - public String getForwardedUrl() { + public @Nullable String getForwardedUrl() { return this.forwardedUrl; } @@ -837,8 +847,7 @@ public void setIncludedUrl(@Nullable String includedUrl) { } } - @Nullable - public String getIncludedUrl() { + public @Nullable String getIncludedUrl() { int count = this.includedUrls.size(); Assert.state(count <= 1, () -> "More than 1 URL included - check getIncludedUrls instead: " + this.includedUrls); diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java index 7b10ea96ae38..e8d4e25eb90f 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpSession.java @@ -29,8 +29,8 @@ import jakarta.servlet.http.HttpSession; import jakarta.servlet.http.HttpSessionBindingEvent; import jakarta.servlet.http.HttpSessionBindingListener; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -45,7 +45,6 @@ * @author Vedran Pavic * @since 1.0.2 */ -@SuppressWarnings("deprecation") public class MockHttpSession implements HttpSession { /** @@ -148,8 +147,7 @@ public int getMaxInactiveInterval() { } @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { assertIsValid(); Assert.notNull(name, "Attribute name must not be null"); return this.attributes.get(name); @@ -240,6 +238,12 @@ public boolean isNew() { return this.isNew; } + @Override + public Accessor getAccessor() { + return sessionConsumer -> sessionConsumer.accept(MockHttpSession.this); + } + + /** * Serialize the attributes of this session into an object that can be * turned into a byte array with standard Java serialization. diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java b/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java index ab0c4ec51c34..272a68021069 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockJspWriter.java @@ -22,8 +22,7 @@ import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.jsp.JspWriter; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Mock implementation of the {@link jakarta.servlet.jsp.JspWriter} class. @@ -36,8 +35,7 @@ public class MockJspWriter extends JspWriter { private final HttpServletResponse response; - @Nullable - private PrintWriter targetWriter; + private @Nullable PrintWriter targetWriter; /** diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java index 781ab7a6e48f..af99ed513edc 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartFile.java @@ -21,8 +21,8 @@ import java.io.IOException; import java.io.InputStream; -import org.springframework.lang.NonNull; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; import org.springframework.util.FileCopyUtils; import org.springframework.web.multipart.MultipartFile; @@ -45,8 +45,7 @@ public class MockMultipartFile implements MultipartFile { private final String originalFilename; - @Nullable - private final String contentType; + private final @Nullable String contentType; private final byte[] content; @@ -56,7 +55,7 @@ public class MockMultipartFile implements MultipartFile { * @param name the name of the file * @param content the content of the file */ - public MockMultipartFile(String name, @Nullable byte[] content) { + public MockMultipartFile(String name, byte @Nullable [] content) { this(name, "", null, content); } @@ -78,7 +77,7 @@ public MockMultipartFile(String name, InputStream contentStream) throws IOExcept * @param content the content of the file */ public MockMultipartFile( - String name, @Nullable String originalFilename, @Nullable String contentType, @Nullable byte[] content) { + String name, @Nullable String originalFilename, @Nullable String contentType, byte @Nullable [] content) { Assert.hasLength(name, "Name must not be empty"); this.name = name; @@ -109,14 +108,12 @@ public String getName() { } @Override - @NonNull public String getOriginalFilename() { return this.originalFilename; } @Override - @Nullable - public String getContentType() { + public @Nullable String getContentType() { return this.contentType; } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java index 41411d366cce..ce07775fe69a 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockMultipartHttpServletRequest.java @@ -28,10 +28,10 @@ import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; import jakarta.servlet.http.Part; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; @@ -96,8 +96,7 @@ public Iterator getFileNames() { } @Override - @Nullable - public MultipartFile getFile(String name) { + public @Nullable MultipartFile getFile(String name) { return this.multipartFiles.getFirst(name); } @@ -118,8 +117,7 @@ public MultiValueMap getMultiFileMap() { } @Override - @Nullable - public String getMultipartContentType(String paramOrFileName) { + public @Nullable String getMultipartContentType(String paramOrFileName) { MultipartFile file = getFile(paramOrFileName); if (file != null) { return file.getContentType(); @@ -156,8 +154,7 @@ public HttpHeaders getRequestHeaders() { } @Override - @Nullable - public HttpHeaders getMultipartHeaders(String paramOrFileName) { + public @Nullable HttpHeaders getMultipartHeaders(String paramOrFileName) { MultipartFile file = getFile(paramOrFileName); if (file != null) { HttpHeaders headers = new HttpHeaders(); diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java index 6bcbf97d57da..5a6544270194 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockPageContext.java @@ -36,8 +36,8 @@ import jakarta.servlet.http.HttpSession; import jakarta.servlet.jsp.JspWriter; import jakarta.servlet.jsp.PageContext; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -63,8 +63,7 @@ public class MockPageContext extends PageContext { private final Map attributes = new LinkedHashMap<>(); - @Nullable - private JspWriter out; + private @Nullable JspWriter out; /** @@ -163,15 +162,13 @@ public void setAttribute(String name, @Nullable Object value, int scope) { } @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { Assert.notNull(name, "Attribute name must not be null"); return this.attributes.get(name); } @Override - @Nullable - public Object getAttribute(String name, int scope) { + public @Nullable Object getAttribute(String name, int scope) { Assert.notNull(name, "Attribute name must not be null"); return switch (scope) { case PAGE_SCOPE -> getAttribute(name); @@ -186,8 +183,7 @@ public Object getAttribute(String name, int scope) { } @Override - @Nullable - public Object findAttribute(String name) { + public @Nullable Object findAttribute(String name) { Object value = getAttribute(name); if (value == null) { value = getAttribute(name, REQUEST_SCOPE); @@ -268,22 +264,7 @@ public JspWriter getOut() { } @Override - @Deprecated - @Nullable - public jakarta.servlet.jsp.el.ExpressionEvaluator getExpressionEvaluator() { - return null; - } - - @Override - @Nullable - public ELContext getELContext() { - return null; - } - - @Override - @Deprecated - @Nullable - public jakarta.servlet.jsp.el.VariableResolver getVariableResolver() { + public @Nullable ELContext getELContext() { return null; } @@ -308,8 +289,7 @@ public ServletResponse getResponse() { } @Override - @Nullable - public Exception getException() { + public @Nullable Exception getException() { return null; } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockPart.java b/spring-test/src/main/java/org/springframework/mock/web/MockPart.java index 1ffe741b7a7a..96b99e21df2f 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockPart.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockPart.java @@ -23,10 +23,10 @@ import java.util.Collections; import jakarta.servlet.http.Part; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,8 +42,7 @@ public class MockPart implements Part { private final String name; - @Nullable - private final String filename; + private final @Nullable String filename; private final byte[] content; @@ -54,7 +53,7 @@ public class MockPart implements Part { * Constructor for a part with a name and content only. * @see #getHeaders() */ - public MockPart(String name, @Nullable byte[] content) { + public MockPart(String name, byte @Nullable [] content) { this(name, null, content); } @@ -62,7 +61,7 @@ public MockPart(String name, @Nullable byte[] content) { * Constructor for a part with a name, filename, and content. * @see #getHeaders() */ - public MockPart(String name, @Nullable String filename, @Nullable byte[] content) { + public MockPart(String name, @Nullable String filename, byte @Nullable [] content) { this(name, filename, content, null); } @@ -71,7 +70,7 @@ public MockPart(String name, @Nullable String filename, @Nullable byte[] content * @since 6.1.2 * @see #getHeaders() */ - public MockPart(String name, @Nullable String filename, @Nullable byte[] content, @Nullable MediaType contentType) { + public MockPart(String name, @Nullable String filename, byte @Nullable [] content, @Nullable MediaType contentType) { Assert.hasLength(name, "'name' must not be empty"); this.name = name; this.filename = filename; @@ -87,14 +86,12 @@ public String getName() { } @Override - @Nullable - public String getSubmittedFileName() { + public @Nullable String getSubmittedFileName() { return this.filename; } @Override - @Nullable - public String getContentType() { + public @Nullable String getContentType() { MediaType contentType = this.headers.getContentType(); return (contentType != null ? contentType.toString() : null); } @@ -120,8 +117,7 @@ public void delete() throws IOException { } @Override - @Nullable - public String getHeader(String name) { + public @Nullable String getHeader(String name) { return this.headers.getFirst(name); } @@ -133,7 +129,7 @@ public Collection getHeaders(String name) { @Override public Collection getHeaderNames() { - return this.headers.keySet(); + return this.headers.headerNames(); } /** diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java index 9c75c2916a8d..47b1e2df91b0 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletConfig.java @@ -23,8 +23,8 @@ import jakarta.servlet.ServletConfig; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -93,8 +93,7 @@ public void addInitParameter(String name, String value) { } @Override - @Nullable - public String getInitParameter(String name) { + public @Nullable String getInitParameter(String name) { Assert.notNull(name, "Parameter name must not be null"); return this.initParameters.get(name); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 38e7dae2ba99..a37917abd49a 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -43,13 +43,13 @@ import jakarta.servlet.descriptor.JspConfigDescriptor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.core.io.ResourceLoader; import org.springframework.http.MediaType; import org.springframework.http.MediaTypeFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -132,18 +132,15 @@ public class MockServletContext implements ServletContext { private final Set declaredRoles = new LinkedHashSet<>(); - @Nullable - private Set sessionTrackingModes; + private @Nullable Set sessionTrackingModes; private final SessionCookieConfig sessionCookieConfig = new MockSessionCookieConfig(); private int sessionTimeout; - @Nullable - private String requestCharacterEncoding; + private @Nullable String requestCharacterEncoding; - @Nullable - private String responseCharacterEncoding; + private @Nullable String responseCharacterEncoding; private final Map filterRegistrations = new LinkedHashMap<>(); @@ -226,8 +223,7 @@ public void registerContext(String contextPath, ServletContext context) { } @Override - @Nullable - public ServletContext getContext(String contextPath) { + public @Nullable ServletContext getContext(String contextPath) { if (this.contextPath.equals(contextPath)) { return this; } @@ -271,8 +267,7 @@ public int getEffectiveMinorVersion() { } @Override - @Nullable - public String getMimeType(String filePath) { + public @Nullable String getMimeType(String filePath) { String extension = StringUtils.getFilenameExtension(filePath); if (this.mimeTypes.containsKey(extension)) { return this.mimeTypes.get(extension).toString(); @@ -295,8 +290,7 @@ public void addMimeType(String fileExtension, MediaType mimeType) { } @Override - @Nullable - public Set getResourcePaths(String path) { + public @Nullable Set getResourcePaths(String path) { String actualPath = (path.endsWith("/") ? path : path + "/"); String resourceLocation = getResourceLocation(actualPath); Resource resource = null; @@ -327,8 +321,7 @@ public Set getResourcePaths(String path) { } @Override - @Nullable - public URL getResource(String path) throws MalformedURLException { + public @Nullable URL getResource(String path) throws MalformedURLException { String resourceLocation = getResourceLocation(path); Resource resource = null; try { @@ -351,8 +344,7 @@ public URL getResource(String path) throws MalformedURLException { } @Override - @Nullable - public InputStream getResourceAsStream(String path) { + public @Nullable InputStream getResourceAsStream(String path) { String resourceLocation = getResourceLocation(path); Resource resource = null; try { @@ -379,8 +371,7 @@ public RequestDispatcher getRequestDispatcher(String path) { } @Override - @Nullable - public RequestDispatcher getNamedDispatcher(String path) { + public @Nullable RequestDispatcher getNamedDispatcher(String path) { return this.namedRequestDispatchers.get(path); } @@ -446,8 +437,7 @@ public void log(String message, Throwable ex) { } @Override - @Nullable - public String getRealPath(String path) { + public @Nullable String getRealPath(String path) { String resourceLocation = getResourceLocation(path); Resource resource = null; try { @@ -469,8 +459,7 @@ public String getServerInfo() { } @Override - @Nullable - public String getInitParameter(String name) { + public @Nullable String getInitParameter(String name) { Assert.notNull(name, "Parameter name must not be null"); return this.initParameters.get(name); } @@ -496,8 +485,7 @@ public void addInitParameter(String name, String value) { } @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { Assert.notNull(name, "Attribute name must not be null"); return this.attributes.get(name); } @@ -534,8 +522,7 @@ public String getServletContextName() { } @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return ClassUtils.getDefaultClassLoader(); } @@ -590,8 +577,7 @@ public void setRequestCharacterEncoding(@Nullable String requestCharacterEncodin } @Override // on Servlet 4.0 - @Nullable - public String getRequestCharacterEncoding() { + public @Nullable String getRequestCharacterEncoding() { return this.requestCharacterEncoding; } @@ -601,8 +587,7 @@ public void setResponseCharacterEncoding(@Nullable String responseCharacterEncod } @Override // on Servlet 4.0 - @Nullable - public String getResponseCharacterEncoding() { + public @Nullable String getResponseCharacterEncoding() { return this.responseCharacterEncoding; } @@ -615,8 +600,7 @@ public void addFilterRegistration(FilterRegistration registration) { } @Override - @Nullable - public FilterRegistration getFilterRegistration(String filterName) { + public @Nullable FilterRegistration getFilterRegistration(String filterName) { return this.filterRegistrations.get(filterName); } @@ -665,8 +649,7 @@ public T createServlet(Class c) throws ServletException { * @see jakarta.servlet.ServletContext#getServletRegistration(java.lang.String) */ @Override - @Nullable - public ServletRegistration getServletRegistration(String servletName) { + public @Nullable ServletRegistration getServletRegistration(String servletName) { return null; } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockSessionCookieConfig.java b/spring-test/src/main/java/org/springframework/mock/web/MockSessionCookieConfig.java index 28c80b522b8b..30bcf6fce7dd 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockSessionCookieConfig.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockSessionCookieConfig.java @@ -21,8 +21,7 @@ import java.util.Map; import jakarta.servlet.SessionCookieConfig; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Mock implementation of the {@link jakarta.servlet.SessionCookieConfig} interface. @@ -33,17 +32,13 @@ */ public class MockSessionCookieConfig implements SessionCookieConfig { - @Nullable - private String name; + private @Nullable String name; - @Nullable - private String domain; + private @Nullable String domain; - @Nullable - private String path; + private @Nullable String path; - @Nullable - private String comment; + private @Nullable String comment; private boolean httpOnly; @@ -60,8 +55,7 @@ public void setName(@Nullable String name) { } @Override - @Nullable - public String getName() { + public @Nullable String getName() { return this.name; } @@ -71,8 +65,7 @@ public void setDomain(@Nullable String domain) { } @Override - @Nullable - public String getDomain() { + public @Nullable String getDomain() { return this.domain; } @@ -82,8 +75,7 @@ public void setPath(@Nullable String path) { } @Override - @Nullable - public String getPath() { + public @Nullable String getPath() { return this.path; } @@ -95,8 +87,7 @@ public void setComment(@Nullable String comment) { @SuppressWarnings("removal") @Override - @Nullable - public String getComment() { + public @Nullable String getComment() { return this.comment; } @@ -136,8 +127,7 @@ public void setAttribute(String name, String value) { } @Override - @Nullable - public String getAttribute(String name) { + public @Nullable String getAttribute(String name) { return this.attributes.get(name); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java b/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java index d0ca4ed48224..2c4158ebff54 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java +++ b/spring-test/src/main/java/org/springframework/mock/web/PassThroughFilterChain.java @@ -24,8 +24,8 @@ import jakarta.servlet.ServletException; import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -42,14 +42,11 @@ */ public class PassThroughFilterChain implements FilterChain { - @Nullable - private Filter filter; + private @Nullable Filter filter; - @Nullable - private FilterChain nextFilterChain; + private @Nullable FilterChain nextFilterChain; - @Nullable - private Servlet servlet; + private @Nullable Servlet servlet; /** diff --git a/spring-test/src/main/java/org/springframework/mock/web/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/package-info.java index 7a01244ba4c2..4673927f82af 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/web/package-info.java @@ -4,9 +4,7 @@ * *

    Useful for testing web contexts and controllers. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.web; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java b/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java index 5461e4030438..9fac4754ab42 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/MockServerRequest.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 java.util.concurrent.ConcurrentHashMap; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -44,7 +45,6 @@ import org.springframework.http.codec.multipart.Part; import org.springframework.http.server.RequestPath; import org.springframework.http.server.reactive.ServerHttpRequest; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.CollectionUtils; import org.springframework.util.LinkedMultiValueMap; @@ -76,8 +76,7 @@ public final class MockServerRequest implements ServerRequest { private final MultiValueMap cookies; - @Nullable - private final Object body; + private final @Nullable Object body; private final Map attributes; @@ -85,22 +84,17 @@ public final class MockServerRequest implements ServerRequest { private final Map pathVariables; - @Nullable - private final WebSession session; + private final @Nullable WebSession session; - @Nullable - private final Principal principal; + private final @Nullable Principal principal; - @Nullable - private final InetSocketAddress remoteAddress; + private final @Nullable InetSocketAddress remoteAddress; - @Nullable - private final InetSocketAddress localAddress; + private final @Nullable InetSocketAddress localAddress; private final List> messageReaders; - @Nullable - private final ServerWebExchange exchange; + private final @Nullable ServerWebExchange exchange; private MockServerRequest(HttpMethod method, URI uri, String contextPath, MockHeaders headers, @@ -133,12 +127,6 @@ public HttpMethod method() { return this.method; } - @Override - @Deprecated - public String methodName() { - return this.method.name(); - } - @Override public URI uri() { return this.uri; @@ -345,8 +333,7 @@ private static class BuilderImpl implements Builder { private MultiValueMap cookies = new LinkedMultiValueMap<>(); - @Nullable - private Object body; + private @Nullable Object body; private Map attributes = new ConcurrentHashMap<>(); @@ -354,22 +341,17 @@ private static class BuilderImpl implements Builder { private Map pathVariables = new LinkedHashMap<>(); - @Nullable - private WebSession session; + private @Nullable WebSession session; - @Nullable - private Principal principal; + private @Nullable Principal principal; - @Nullable - private InetSocketAddress remoteAddress; + private @Nullable InetSocketAddress remoteAddress; - @Nullable - private InetSocketAddress localAddress; + private @Nullable InetSocketAddress localAddress; private List> messageReaders = HandlerStrategies.withDefaults().messageReaders(); - @Nullable - private ServerWebExchange exchange; + private @Nullable ServerWebExchange exchange; @Override public Builder method(HttpMethod method) { @@ -569,8 +551,7 @@ public Optional contentType() { } @Override - @Nullable - public InetSocketAddress host() { + public @Nullable InetSocketAddress host() { return delegate().getHost(); } diff --git a/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/package-info.java index 91fe0741840d..41100d28ee8c 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/web/reactive/function/server/package-info.java @@ -4,9 +4,7 @@ *

    Useful for testing router and handler functions. * */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.web.reactive.function.server; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 e2cf0fae8f54..ca8a064888a0 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 @@ -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,10 +16,13 @@ package org.springframework.mock.web.server; +import java.security.Principal; + +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; +import org.springframework.context.ApplicationContext; import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.lang.Nullable; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; import org.springframework.mock.http.server.reactive.MockServerHttpResponse; import org.springframework.web.server.WebSession; @@ -39,9 +42,19 @@ */ public final class MockServerWebExchange extends DefaultServerWebExchange { - private MockServerWebExchange(MockServerHttpRequest request, WebSessionManager sessionManager) { - super(request, new MockServerHttpResponse(), sessionManager, - ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver()); + private final Mono principalMono; + + + private MockServerWebExchange( + MockServerHttpRequest request, @Nullable WebSessionManager sessionManager, + @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(); } @@ -50,6 +63,16 @@ public MockServerHttpResponse getResponse() { 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. @@ -98,8 +121,11 @@ public static class Builder { private final MockServerHttpRequest request; - @Nullable - private WebSessionManager sessionManager; + private @Nullable WebSessionManager sessionManager; + + private @Nullable ApplicationContext applicationContext; + + private @Nullable Principal principal; public Builder(MockServerHttpRequest request) { this.request = request; @@ -127,12 +153,32 @@ public Builder sessionManager(WebSessionManager sessionManager) { return this; } + /** + * Provide the {@code ApplicationContext} to expose through the exchange. + * @param applicationContext the context to use + * @since 6.2.5 + */ + public Builder applicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + 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 != null ? this.sessionManager : new DefaultWebSessionManager()); + return new MockServerWebExchange( + this.request, this.sessionManager, this.applicationContext, this.principal); } } diff --git a/spring-test/src/main/java/org/springframework/mock/web/server/MockWebSession.java b/spring-test/src/main/java/org/springframework/mock/web/server/MockWebSession.java index 0a13b853ee3d..e8d795c6db91 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/server/MockWebSession.java +++ b/spring-test/src/main/java/org/springframework/mock/web/server/MockWebSession.java @@ -21,9 +21,9 @@ import java.time.Instant; import java.util.Map; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.web.server.WebSession; import org.springframework.web.server.session.InMemoryWebSessionStore; diff --git a/spring-test/src/main/java/org/springframework/mock/web/server/package-info.java b/spring-test/src/main/java/org/springframework/mock/web/server/package-info.java index 66a15310ee18..e290525c8d79 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/server/package-info.java +++ b/spring-test/src/main/java/org/springframework/mock/web/server/package-info.java @@ -1,9 +1,7 @@ /** * Mock implementations of Spring's reactive server web API abstractions. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.mock.web.server; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 c4cb54cbfa5f..0f4dedf3ed20 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 @@ * {@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 6e602f53380a..f1e509c7294f 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 @@ *

    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 9af1ec7492e2..96689bb0f2fe 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 @@ * @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 d6ce0a75ea06..33551d5f4429 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. @@ -16,7 +16,7 @@ package org.springframework.test.annotation; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** *

    @@ -40,7 +40,11 @@ * @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 { /** @@ -49,7 +53,6 @@ public interface ProfileValueSource { * @return the String value of the profile value, or {@code null} * if there is no profile value with that key */ - @Nullable - String get(String key); + @Nullable String get(String key); } 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 37af23cb3d2f..667f5f8dd13e 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 @@ * @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 26957c49f25e..472f65259bf0 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. @@ -20,10 +20,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.ReflectionUtils; @@ -38,7 +38,9 @@ * @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 dfa062ceb2ba..7ff46c35a72f 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 @@ * @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 9d2cbcd8dfc0..a0b28e10e3f8 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 @@ * 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 4a0f627664a7..b021904b6c16 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 @@ * @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 e0b4ac927a97..c61fb8e8cf42 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 @@ * * @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 3e5ca273ebc7..b40597c076b3 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 @@ * @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/annotation/package-info.java b/spring-test/src/main/java/org/springframework/test/annotation/package-info.java index 0e93e33cbd7f..84e981518d8f 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for annotation-driven tests. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 97d5959319a1..5f9dc72cd2cc 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 @@ *

    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/ApplicationContextFailureProcessor.java b/spring-test/src/main/java/org/springframework/test/context/ApplicationContextFailureProcessor.java index 2b22556d9ae4..4fc8214fb962 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ApplicationContextFailureProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/ApplicationContextFailureProcessor.java @@ -16,8 +16,9 @@ package org.springframework.test.context; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; /** * Strategy for components that process failures related to application contexts diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index 5a35391eafa7..8af79ed50fb1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -23,10 +23,10 @@ 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.core.log.LogMessage; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContextAnnotationUtils.AnnotationDescriptor; import org.springframework.util.ClassUtils; @@ -168,8 +168,7 @@ attribute or make the default bootstrapper class available.""".formatted(clazz), } } - @Nullable - private static Class resolveExplicitTestContextBootstrapper(Class testClass) { + private static @Nullable Class resolveExplicitTestContextBootstrapper(Class testClass) { Set annotations = new LinkedHashSet<>(); AnnotationDescriptor descriptor = TestContextAnnotationUtils.findAnnotationDescriptor(testClass, BootstrapWith.class); 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 17b5c61ace0a..da351f79434e 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 @@ * 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/CacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java index 9e5def712c82..276a23620b66 100644 --- a/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/CacheAwareContextLoaderDelegate.java @@ -16,9 +16,10 @@ package org.springframework.test.context; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; /** 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 90bb738b7774..e02360b9143a 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 @@ *

    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 @@ *

    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/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java index 73643729ef4f..4d0e4f013afc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java @@ -20,13 +20,13 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContextInitializer; import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -63,8 +63,7 @@ public class ContextConfigurationAttributes { private final boolean inheritInitializers; - @Nullable - private final String name; + private final @Nullable String name; private final Class contextLoaderClass; @@ -305,8 +304,7 @@ public boolean isInheritInitializers() { * @since 3.2.2 * @see ContextConfiguration#name() */ - @Nullable - public String getName() { + public @Nullable String getName() { return this.name; } diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java index e8b35ff2c514..3541667950b7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java @@ -18,7 +18,7 @@ import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Factory for creating {@link ContextCustomizer ContextCustomizers}. @@ -56,7 +56,6 @@ public interface ContextCustomizerFactory { * @return a {@link ContextCustomizer} or {@code null} if no customizer should * be used */ - @Nullable - ContextCustomizer createContextCustomizer(Class testClass, List configAttributes); + @Nullable ContextCustomizer createContextCustomizer(Class testClass, List configAttributes); } 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 0785c965f8c8..8bc139884b9c 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 @@ * 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 @@ * }

    * *

    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 @@ * 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 @@ * 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 @@ * ) * 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/DefaultMethodInvoker.java b/spring-test/src/main/java/org/springframework/test/context/DefaultMethodInvoker.java index a0f96ed47e0b..b6256787590e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/DefaultMethodInvoker.java +++ b/spring-test/src/main/java/org/springframework/test/context/DefaultMethodInvoker.java @@ -21,8 +21,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -40,8 +40,7 @@ final class DefaultMethodInvoker implements MethodInvoker { @Override - @Nullable - public Object invoke(Method method, @Nullable Object target) throws Exception { + public @Nullable Object invoke(Method method, @Nullable Object target) throws Exception { Assert.notNull(method, "Method must not be null"); try { diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java index 26837b83f59a..99eea9824f0a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,13 +23,14 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -88,25 +89,28 @@ public class MergedContextConfiguration implements Serializable { private final Class[] classes; + @SuppressWarnings("serial") private final Set>> contextInitializerClasses; private final String[] activeProfiles; + @SuppressWarnings("serial") private final List propertySourceDescriptors; private final String[] propertySourceLocations; private final String[] propertySourceProperties; + @SuppressWarnings("serial") private final Set contextCustomizers; + @SuppressWarnings("serial") private final ContextLoader contextLoader; - @Nullable - private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; + @SuppressWarnings("serial") + private final @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; - @Nullable - private final MergedContextConfiguration parent; + private final @Nullable MergedContextConfiguration parent; /** @@ -118,8 +122,8 @@ public class MergedContextConfiguration implements Serializable { * @param activeProfiles the merged active bean definition profiles * @param contextLoader the resolved {@code ContextLoader} */ - public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, - @Nullable String[] activeProfiles, ContextLoader contextLoader) { + public MergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, + String @Nullable [] activeProfiles, ContextLoader contextLoader) { this(testClass, locations, classes, null, activeProfiles, contextLoader); } @@ -134,9 +138,9 @@ public MergedContextConfiguration(Class testClass, @Nullable String[] locatio * @param activeProfiles the merged active bean definition profiles * @param contextLoader the resolved {@code ContextLoader} */ - public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public MergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, ContextLoader contextLoader) { + String @Nullable [] activeProfiles, ContextLoader contextLoader) { this(testClass, locations, classes, contextInitializerClasses, activeProfiles, contextLoader, null, null); } @@ -155,9 +159,9 @@ public MergedContextConfiguration(Class testClass, @Nullable String[] locatio * @param parent the parent configuration or {@code null} if there is no parent * @since 3.2.2 */ - public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public MergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, ContextLoader contextLoader, + String @Nullable [] activeProfiles, ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { @@ -204,10 +208,10 @@ public MergedContextConfiguration(MergedContextConfiguration mergedConfig) { * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ @Deprecated(since = "6.1") - public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public MergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, - @Nullable String[] propertySourceProperties, ContextLoader contextLoader, + String @Nullable [] activeProfiles, String @Nullable [] propertySourceLocations, + String @Nullable [] propertySourceProperties, ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { @@ -244,10 +248,10 @@ public MergedContextConfiguration(Class testClass, @Nullable String[] locatio * {@link #MergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ @Deprecated(since = "6.1") - public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public MergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, - @Nullable String[] propertySourceProperties, @Nullable Set contextCustomizers, + String @Nullable [] activeProfiles, String @Nullable [] propertySourceLocations, + String @Nullable [] propertySourceProperties, @Nullable Set contextCustomizers, ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { @@ -280,10 +284,10 @@ public MergedContextConfiguration(Class testClass, @Nullable String[] locatio * @param parent the parent configuration or {@code null} if there is no parent * @since 6.1 */ - public MergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public MergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, List propertySourceDescriptors, - @Nullable String[] propertySourceProperties, @Nullable Set contextCustomizers, + String @Nullable [] activeProfiles, List propertySourceDescriptors, + String @Nullable [] propertySourceProperties, @Nullable Set contextCustomizers, ContextLoader contextLoader, @Nullable CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { @@ -443,8 +447,7 @@ public ContextLoader getContextLoader() { * @since 3.2.2 * @see #getParentApplicationContext() */ - @Nullable - public MergedContextConfiguration getParent() { + public @Nullable MergedContextConfiguration getParent() { return this.parent; } @@ -457,8 +460,7 @@ public MergedContextConfiguration getParent() { * @since 3.2.2 * @see #getParent() */ - @Nullable - public ApplicationContext getParentApplicationContext() { + public @Nullable ApplicationContext getParentApplicationContext() { if (this.parent == null) { return null; } @@ -575,11 +577,11 @@ public String toString() { } - protected static String[] processStrings(@Nullable String[] array) { + protected static String[] processStrings(String @Nullable [] array) { return (array != null ? array : EMPTY_STRING_ARRAY); } - private static Class[] processClasses(@Nullable Class[] classes) { + private static Class[] processClasses(Class @Nullable [] classes) { return (classes != null ? classes : EMPTY_CLASS_ARRAY); } @@ -597,7 +599,7 @@ private static Set processContextCustomizers( Collections.unmodifiableSet(contextCustomizers) : EMPTY_CONTEXT_CUSTOMIZERS); } - private static String[] processActiveProfiles(@Nullable String[] activeProfiles) { + private static String[] processActiveProfiles(String @Nullable [] activeProfiles) { if (activeProfiles == null) { return EMPTY_STRING_ARRAY; } diff --git a/spring-test/src/main/java/org/springframework/test/context/MethodInvoker.java b/spring-test/src/main/java/org/springframework/test/context/MethodInvoker.java index 247351e3b8ca..ea88c655d9c1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MethodInvoker.java +++ b/spring-test/src/main/java/org/springframework/test/context/MethodInvoker.java @@ -18,7 +18,7 @@ import java.lang.reflect.Method; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code MethodInvoker} defines a generic API for invoking a {@link Method} @@ -66,7 +66,6 @@ public interface MethodInvoker { * @return the value returned from the method invocation, potentially {@code null} * @throws Exception if any error occurs */ - @Nullable - Object invoke(Method method, @Nullable Object target) throws Exception; + @Nullable Object invoke(Method method, @Nullable Object target) throws Exception; } 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 9b716cc7f4ef..700826598a78 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 @@ -26,8 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code @NestedTestConfiguration} is an annotation that can be applied to a test @@ -76,6 +75,7 @@ *

      *
    • {@link BootstrapWith @BootstrapWith}
    • *
    • {@link TestExecutionListeners @TestExecutionListeners}
    • + *
    • {@link ContextCustomizerFactories @ContextCustomizerFactories}
    • *
    • {@link ContextConfiguration @ContextConfiguration}
    • *
    • {@link ContextHierarchy @ContextHierarchy}
    • *
    • {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}
    • @@ -164,8 +164,7 @@ enum EnclosingConfiguration { * @return the corresponding enum constant or {@code null} if not found * @see EnclosingConfiguration#valueOf(String) */ - @Nullable - public static EnclosingConfiguration from(@Nullable String name) { + public static @Nullable EnclosingConfiguration from(@Nullable String name) { if (name == null) { return null; } 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 3285ca6f5acc..19c28a9fd81a 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. @@ -26,8 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code @TestConstructor} is an annotation that can be applied to a test class @@ -39,9 +38,8 @@ * on a test class, the default test constructor autowire mode will be * used. See {@link #TEST_CONSTRUCTOR_AUTOWIRE_MODE_PROPERTY_NAME} for details on * how to change the default mode. Note, however, that a local declaration of - * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} - * {@link jakarta.inject.Inject @jakarta.inject.Inject}, or - * {@link javax.inject.Inject @javax.inject.Inject} on a constructor takes + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} or + * {@link jakarta.inject.Inject @jakarta.inject.Inject} on a constructor takes * precedence over both {@code @TestConstructor} and the default mode. * *

      This annotation may be used as a meta-annotation to create custom @@ -55,15 +53,13 @@ * {@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 * @see org.springframework.beans.factory.annotation.Autowired @Autowired * @see jakarta.inject.Inject @jakarta.inject.Inject - * @see javax.inject.Inject @javax.inject.Inject * @see org.springframework.test.context.junit.jupiter.SpringExtension SpringExtension * @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig @SpringJUnitConfig * @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig @SpringJUnitWebConfig @@ -91,7 +87,7 @@ *

      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 @@ -109,7 +105,6 @@ * @see #TEST_CONSTRUCTOR_AUTOWIRE_MODE_PROPERTY_NAME * @see org.springframework.beans.factory.annotation.Autowired @Autowired * @see jakarta.inject.Inject @jakarta.inject.Inject - * @see javax.inject.Inject @javax.inject.Inject * @see AutowireMode#ALL * @see AutowireMode#ANNOTATED */ @@ -126,9 +121,8 @@ enum AutowireMode { /** * All test constructor parameters will be autowired as if the constructor * itself were annotated with - * {@link org.springframework.beans.factory.annotation.Autowired @Autowired}, - * {@link jakarta.inject.Inject @jakarta.inject.Inject}, or - * {@link javax.inject.Inject @javax.inject.Inject}. + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} or + * {@link jakarta.inject.Inject @jakarta.inject.Inject}. * @see #ANNOTATED */ ALL, @@ -140,9 +134,8 @@ enum AutowireMode { * {@link org.springframework.beans.factory.annotation.Qualifier @Qualifier}, * or {@link org.springframework.beans.factory.annotation.Value @Value}, * or if the constructor itself is annotated with - * {@link org.springframework.beans.factory.annotation.Autowired @Autowired}, - * {@link jakarta.inject.Inject @jakarta.inject.Inject}, or - * {@link javax.inject.Inject @javax.inject.Inject}. + * {@link org.springframework.beans.factory.annotation.Autowired @Autowired} or + * {@link jakarta.inject.Inject @jakarta.inject.Inject}. * @see #ALL */ ANNOTATED; @@ -156,8 +149,7 @@ enum AutowireMode { * @since 5.3 * @see AutowireMode#valueOf(String) */ - @Nullable - public static AutowireMode from(@Nullable String name) { + public static @Nullable AutowireMode from(@Nullable String name) { if (name == null) { return null; } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContext.java b/spring-test/src/main/java/org/springframework/test/context/TestContext.java index 3e6d5109c13d..b8234f3fe163 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContext.java @@ -20,10 +20,11 @@ import java.lang.reflect.Method; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationEvent; import org.springframework.core.AttributeAccessor; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; /** @@ -125,8 +126,7 @@ default void publishEvent(Function even * @return the exception that was thrown, or {@code null} if no exception was thrown * @see #updateState(Object, Method, Throwable) */ - @Nullable - Throwable getTestException(); + @Nullable Throwable getTestException(); /** * Call this method to signal that the {@linkplain ApplicationContext application diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java index 39e70bd32f7d..65c336eaa44e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextAnnotationUtils.java @@ -22,6 +22,8 @@ import java.util.Set; import java.util.function.Predicate; +import org.jspecify.annotations.Nullable; + import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.annotation.AnnotationUtils; @@ -34,7 +36,6 @@ import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.NestedTestConfiguration.EnclosingConfiguration; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -83,8 +84,7 @@ public abstract class TestContextAnnotationUtils { private static final ConcurrentLruCache, EnclosingConfiguration> cachedEnclosingConfigurationModes = new ConcurrentLruCache<>(32, TestContextAnnotationUtils::lookUpEnclosingConfiguration); - @Nullable - private static volatile EnclosingConfiguration defaultEnclosingConfigurationMode; + private static volatile @Nullable EnclosingConfiguration defaultEnclosingConfigurationMode; /** @@ -128,13 +128,11 @@ public static boolean hasAnnotation(Class clazz, Class * @see #findAnnotationDescriptor(Class, Class) * @see #searchEnclosingClass(Class) */ - @Nullable - public static T findMergedAnnotation(Class clazz, Class annotationType) { + public static @Nullable T findMergedAnnotation(Class clazz, Class annotationType) { return findMergedAnnotation(clazz, annotationType, TestContextAnnotationUtils::searchEnclosingClass); } - @Nullable - private static T findMergedAnnotation(Class clazz, Class annotationType, + private static @Nullable T findMergedAnnotation(Class clazz, Class annotationType, Predicate> searchEnclosingClass) { return MergedAnnotations.search(SearchStrategy.TYPE_HIERARCHY) @@ -220,8 +218,7 @@ public static Set getMergedRepeatableAnnotations( * otherwise {@code null} * @see #findAnnotationDescriptorForTypes(Class, Class...) */ - @Nullable - public static AnnotationDescriptor findAnnotationDescriptor( + public static @Nullable AnnotationDescriptor findAnnotationDescriptor( Class clazz, Class annotationType) { Assert.notNull(annotationType, "Annotation type must not be null"); @@ -241,8 +238,7 @@ public static AnnotationDescriptor findAnnotationDescr * @return the corresponding annotation descriptor if the annotation was found; * otherwise {@code null} */ - @Nullable - private static AnnotationDescriptor findAnnotationDescriptor( + private static @Nullable AnnotationDescriptor findAnnotationDescriptor( @Nullable Class clazz, Class annotationType, Predicate> searchEnclosingClass, Set visited) { @@ -327,8 +323,7 @@ private static AnnotationDescriptor findAnnotationDesc * @see #findAnnotationDescriptor(Class, Class) */ @SuppressWarnings("unchecked") - @Nullable - public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes( + public static @Nullable UntypedAnnotationDescriptor findAnnotationDescriptorForTypes( Class clazz, Class... annotationTypes) { assertNonEmptyAnnotationTypeArray(annotationTypes, "The list of annotation types must not be empty"); @@ -345,8 +340,7 @@ public static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes( * @return the corresponding annotation descriptor if one of the annotations * was found; otherwise {@code null} */ - @Nullable - private static UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(@Nullable Class clazz, + private static @Nullable UntypedAnnotationDescriptor findAnnotationDescriptorForTypes(@Nullable Class clazz, Class[] annotationTypes, Set visited) { if (clazz == null || Object.class == clazz) { @@ -561,8 +555,7 @@ Class getAnnotationType() { * @return the next corresponding annotation descriptor if the annotation * was found; otherwise {@code null} */ - @Nullable - public AnnotationDescriptor next() { + public @Nullable AnnotationDescriptor next() { // Declared on a superclass? AnnotationDescriptor descriptor = findAnnotationDescriptor(getRootDeclaringClass().getSuperclass(), getAnnotationType()); @@ -640,8 +633,7 @@ public static class UntypedAnnotationDescriptor extends AnnotationDescriptorThis 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 f53079a8b7b3..5cfbf2f35840 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 @@ * 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 8aee40286647..4ff318d51d47 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 @@ * 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/AotTestAttributes.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java index cfb243b4915c..b1701caf4d2d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributes.java @@ -16,8 +16,9 @@ package org.springframework.test.context.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.AotDetector; -import org.springframework.lang.Nullable; /** * Holder for metadata specific to ahead-of-time (AOT) support in the Spring @@ -117,14 +118,13 @@ default void setAttribute(String name, boolean value) { * @see #getBoolean(String) * @see #setAttribute(String, String) */ - @Nullable - String getString(String name); + @Nullable String getString(String name); /** * Retrieve the attribute value for the given name as a {@code boolean}. * @param name the unique attribute name * @return {@code true} if the attribute is set to "true" (ignoring case), - * {@code} false otherwise + * {@code false} otherwise * @see #getString(String) * @see #setAttribute(String, String) * @see Boolean#parseBoolean(String) diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java index 1414083bd66b..27c6ce61cf18 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestAttributesFactory.java @@ -19,8 +19,9 @@ import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.AotDetector; -import org.springframework.lang.Nullable; /** * Factory for {@link AotTestAttributes}. @@ -30,8 +31,7 @@ */ final class AotTestAttributesFactory { - @Nullable - private static volatile Map attributes; + private static volatile @Nullable Map attributes; private AotTestAttributesFactory() { diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java index 15b95e18ec3d..b0355698000d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializers.java @@ -19,10 +19,11 @@ import java.util.Map; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.AotDetector; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.lang.Nullable; /** * {@code AotTestContextInitializers} provides mappings from test classes to @@ -76,8 +77,7 @@ public boolean isSupportedTestClass(Class testClass) { * @see #isSupportedTestClass(Class) * @see #getContextInitializerClass(Class) */ - @Nullable - public ApplicationContextInitializer getContextInitializer(Class testClass) { + public @Nullable ApplicationContextInitializer getContextInitializer(Class testClass) { Supplier> supplier = this.contextInitializers.get(testClass.getName()); return (supplier != null ? supplier.get() : null); @@ -91,8 +91,7 @@ public ApplicationContextInitializer getContextI * @see #isSupportedTestClass(Class) * @see #getContextInitializer(Class) */ - @Nullable - public Class> getContextInitializerClass(Class testClass) { + public @Nullable Class> getContextInitializerClass(Class testClass) { return this.contextInitializerClasses.get(testClass.getName()); } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java index e130258a8ef2..1b742c50ae6f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/AotTestContextInitializersFactory.java @@ -19,10 +19,11 @@ import java.util.Map; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.AotDetector; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.lang.Nullable; /** * Factory for {@link AotTestContextInitializers}. @@ -32,11 +33,9 @@ */ final class AotTestContextInitializersFactory { - @Nullable - private static volatile Map>> contextInitializers; + private static volatile @Nullable Map>> contextInitializers; - @Nullable - private static volatile Map>> contextInitializerClasses; + private static volatile @Nullable Map>> contextInitializerClasses; private AotTestContextInitializersFactory() { diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java b/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java index 966485d5b519..d3f663b018bc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/DefaultAotTestAttributes.java @@ -18,8 +18,9 @@ import java.util.Map; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.AotDetector; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -54,8 +55,7 @@ public void removeAttribute(String name) { } @Override - @Nullable - public String getString(String name) { + public @Nullable String getString(String name) { return this.attributes.get(name); } diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java index 82803a5b4440..25455a3d638c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/MergedContextConfigurationRuntimeHints.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2024 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -144,7 +144,7 @@ private void registerClasspathResourceDirectoryStructure(String directory, Runti if (!pattern.endsWith(SLASH)) { pattern += SLASH; } - pattern += "*"; + pattern += "**"; runtimeHints.resources().registerPattern(pattern); } } 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 953aa1094ff0..490416d4b9cb 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 @@ else if (logger.isWarnEnabled()) { 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 @@ ClassName processAheadOfTime(MergedContextConfiguration mergedConfig, * 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/aot/TestContextGenerationContext.java b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java index 65e630927ae3..3e2e63935d07 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/TestContextGenerationContext.java @@ -16,11 +16,12 @@ package org.springframework.test.context.aot; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.generate.ClassNameGenerator; import org.springframework.aot.generate.DefaultGenerationContext; import org.springframework.aot.generate.GeneratedFiles; import org.springframework.aot.hint.RuntimeHints; -import org.springframework.lang.Nullable; /** * Extension of {@link DefaultGenerationContext} with a custom implementation of @@ -31,8 +32,7 @@ */ class TestContextGenerationContext extends DefaultGenerationContext { - @Nullable - private final String featureName; + private final @Nullable String featureName; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/aot/package-info.java b/spring-test/src/main/java/org/springframework/test/context/aot/package-info.java index d673bff93ead..7339dd9e83fc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/aot/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/aot/package-info.java @@ -1,9 +1,7 @@ /** * Ahead-of-time (AOT) support for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.aot; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 11d282f405df..6446e6c22689 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 @@ -22,6 +22,8 @@ import java.util.LinkedHashSet; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.aop.scope.ScopedProxyUtils; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; @@ -39,7 +41,6 @@ import org.springframework.context.aot.AbstractAotProcessor; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -152,6 +153,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be // an existing bean definition. if (beanFactory.containsBeanDefinition(beanName)) { existingBeanDefinition = beanFactory.getBeanDefinition(beanName); + setQualifiedElement(existingBeanDefinition, handler); } } else { @@ -166,6 +168,7 @@ private void replaceOrCreateBean(ConfigurableListableBeanFactory beanFactory, Be 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(); @@ -290,8 +293,7 @@ most specific type possible (for example, the concrete implementation type).""" this.beanOverrideRegistry.registerBeanOverrideHandler(handler, beanName); } - @Nullable - private static String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler, + private static @Nullable String getBeanNameForType(ConfigurableListableBeanFactory beanFactory, BeanOverrideHandler handler, boolean requireExistingBean) { Field field = handler.getField(); @@ -359,8 +361,7 @@ private static Set getExistingBeanNamesByType(ConfigurableListableBeanFa * @since 6.2.3 * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determineAutowireCandidate */ - @Nullable - private static String determineUniqueCandidate(ConfigurableListableBeanFactory beanFactory, + private static @Nullable String determineUniqueCandidate(ConfigurableListableBeanFactory beanFactory, Set candidateNames, ResolvableType beanType, @Nullable Field field) { // Step 0: none or only one @@ -395,8 +396,7 @@ private static String determineUniqueCandidate(ConfigurableListableBeanFactory b * @return the name of the primary candidate, or {@code null} if none found * @see org.springframework.beans.factory.support.DefaultListableBeanFactory#determinePrimaryCandidate */ - @Nullable - private static String determinePrimaryCandidate(ConfigurableListableBeanFactory beanFactory, + private static @Nullable String determinePrimaryCandidate(ConfigurableListableBeanFactory beanFactory, Set candidateBeanNames, Class beanType) { if (candidateBeanNames.isEmpty()) { @@ -450,10 +450,25 @@ private static String determinePrimaryCandidate(ConfigurableListableBeanFactory 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 0820042209d9..3e2d24163b9d 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 @@ */ 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 @@ public void customizeContext(ConfigurableApplicationContext context, MergedConte // 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 dfa9c9589eef..bdb6dcca6ca5 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 @@ -20,7 +20,8 @@ import java.util.List; import java.util.Set; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.util.Assert; @@ -38,23 +39,28 @@ class BeanOverrideContextCustomizerFactory implements ContextCustomizerFactory { @Override - @Nullable - public BeanOverrideContextCustomizer createContextCustomizer(Class testClass, + 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 06eeaaf1fa97..0d9c6a762239 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 @@ -30,6 +30,8 @@ import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.BiConsumer; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.SingletonBeanRegistry; @@ -37,7 +39,6 @@ import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; @@ -77,27 +78,63 @@ public abstract class BeanOverrideHandler { Comparator.> comparingInt(MergedAnnotation::getDistance).reversed(); - @Nullable - private final Field field; + private final @Nullable Field field; private final Set qualifierAnnotations; private final ResolvableType beanType; - @Nullable - private final String beanName; + 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; } /** @@ -146,30 +183,32 @@ private static List findHandlers(Class testClass, boolea * @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. @@ -215,10 +254,9 @@ private static void processElement(AnnotatedElement element, Class testClass, /** - * Get the annotated {@link Field}. + * Get the {@link Field} annotated with {@link BeanOverride @BeanOverride}. */ - @Nullable - public final Field getField() { + public final @Nullable Field getField() { return this.field; } @@ -233,11 +271,25 @@ public final ResolvableType getBeanType() { * Get the bean name to override, or {@code null} to look for a single * matching bean of type {@link #getBeanType()}. */ - @Nullable - public final String getBeanName() { + public final @Nullable String getBeanName() { 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. @@ -311,6 +363,7 @@ public boolean equals(Object other) { 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; } @@ -330,7 +383,7 @@ public boolean equals(Object other) { @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)); } @@ -341,6 +394,7 @@ public String toString() { .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 3afc7c885af1..fe7d4c8480f6 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 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 @@ */ 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 @@ Object wrapBeanIfNecessary(Object bean, String beanName) { 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 736223358cce..4d934980dfae 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 @@ private static void injectFields(TestContext testContext) { 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 9393a17ed0cb..837b975b331f 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 @@ * } * } * + *

    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 @@ */ 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 20df24ea8850..7156e7f6369d 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 @@ -21,10 +21,11 @@ import java.lang.reflect.Method; import java.util.Objects; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.util.ReflectionUtils; @@ -43,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; } @@ -90,6 +91,7 @@ public String toString() { .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 a47d491b8452..601afcec098f 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 @@ public TestBeanOverrideHandler createHandler(Annotation overrideAnnotation, Clas } 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/convention/package-info.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/package-info.java index 59256e3fe604..cf410d0482c5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/package-info.java @@ -3,9 +3,7 @@ * in the test class. This allows defining a custom instance for the bean * straight from the test class. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.bean.override.convention; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 061a3bff4343..e445c435c198 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 @@ -18,10 +18,11 @@ import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.test.context.bean.override.BeanOverrideStrategy; @@ -39,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); } @@ -92,6 +94,7 @@ public String toString() { .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 46d5c0917f9c..4c95a21518fa 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 @@ * 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 @@ */ 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 449e487e88ba..1a5cd191dde6 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 @@ -23,6 +23,7 @@ import java.util.Objects; import java.util.Set; +import org.jspecify.annotations.Nullable; import org.mockito.Answers; import org.mockito.MockSettings; import org.mockito.Mockito; @@ -30,7 +31,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.util.Assert; @@ -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 @@ public String toString() { .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/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 0064c4916fc2..4d42c18b81ba 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.mockito.Mockito; import org.springframework.beans.factory.BeanFactory; @@ -32,7 +33,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Ordered; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.util.ClassUtils; @@ -76,8 +76,7 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList * @see #mockitoPresent * @see #isEnabled() */ - @Nullable - private static volatile Boolean mockitoInitialized; + private static volatile @Nullable Boolean mockitoInitialized; /** @@ -136,8 +135,7 @@ private static void resetMocks(ConfigurableApplicationContext applicationContext } } - @Nullable - private static Object getBean(ConfigurableListableBeanFactory beanFactory, String beanName) { + private static @Nullable Object getBean(ConfigurableListableBeanFactory beanFactory, String beanName) { try { if (isStandardBeanOrSingletonFactoryBean(beanFactory, beanName)) { return beanFactory.getBean(beanName); 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 e42c0b4563ba..aa2d8cbb59e0 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 @@ * {@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 @@ */ 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 ce3f11cbe204..d04712e0cb92 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 @@ -19,6 +19,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Proxy; +import org.jspecify.annotations.Nullable; import org.mockito.AdditionalAnswers; import org.mockito.MockSettings; import org.mockito.Mockito; @@ -27,7 +28,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.test.context.bean.override.BeanOverrideStrategy; import org.springframework.util.Assert; @@ -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/bean/override/mockito/package-info.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/package-info.java index 15330b2b514a..1d4c16cd8e74 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/package-info.java @@ -1,9 +1,7 @@ /** * Bean overriding mechanism based on Mockito mocking and spying. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.bean.override.mockito; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java index 4969d011ca97..4b5ab0a176d5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/package-info.java @@ -1,9 +1,7 @@ /** * Support case-by-case Bean overriding in Spring tests. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.bean.override; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java index 8ee994362320..712230d6627a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/AotMergedContextConfiguration.java @@ -18,11 +18,12 @@ import java.util.Collections; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContextInitializer; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.MergedContextConfiguration; diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java index 7d64249ce654..f4f4a6fe1983 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java @@ -16,8 +16,9 @@ package org.springframework.test.context.cache; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.MergedContextConfiguration; @@ -95,8 +96,7 @@ public interface ContextCache { * if not found in the cache * @see #remove */ - @Nullable - ApplicationContext get(MergedContextConfiguration key); + @Nullable ApplicationContext get(MergedContextConfiguration key); /** * Explicitly add an {@code ApplicationContext} instance to the cache diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java index c9601be99297..be0bd587a72b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultCacheAwareContextLoaderDelegate.java @@ -20,13 +20,13 @@ 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.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.support.GenericApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.ApplicationContextFailureProcessor; import org.springframework.test.context.CacheAwareContextLoaderDelegate; diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java index 99342ec4964d..6821dd8ff7d4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java @@ -28,11 +28,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.util.Assert; @@ -121,8 +121,7 @@ public boolean contains(MergedContextConfiguration key) { } @Override - @Nullable - public ApplicationContext get(MergedContextConfiguration key) { + public @Nullable ApplicationContext get(MergedContextConfiguration key) { Assert.notNull(key, "Key must not be null"); ApplicationContext context = this.contextMap.get(key); if (context == null) { diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/package-info.java b/spring-test/src/main/java/org/springframework/test/context/cache/package-info.java index e015cbf75881..ef6e1811eb26 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/package-info.java @@ -1,9 +1,7 @@ /** * Support for context caching within the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.cache; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java index 91c4a9a8b217..c98be07fc8e1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java +++ b/spring-test/src/main/java/org/springframework/test/context/event/ApplicationEventsHolder.java @@ -16,7 +16,8 @@ package org.springframework.test.context.event; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.Assert; /** @@ -57,8 +58,7 @@ private ApplicationEventsHolder() { * Get the {@link ApplicationEvents} for the current thread. * @return the current {@code ApplicationEvents}, or {@code null} if not registered */ - @Nullable - public static ApplicationEvents getApplicationEvents() { + public static @Nullable ApplicationEvents getApplicationEvents() { return applicationEvents.get(); } diff --git a/spring-test/src/main/java/org/springframework/test/context/event/annotation/package-info.java b/spring-test/src/main/java/org/springframework/test/context/event/annotation/package-info.java index f2a580e6ae46..99f39ac1d3b3 100644 --- a/spring-test/src/main/java/org/springframework/test/context/event/annotation/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/event/annotation/package-info.java @@ -1,9 +1,7 @@ /** * Test execution event annotations for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.event.annotation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/event/package-info.java b/spring-test/src/main/java/org/springframework/test/context/event/package-info.java index 1cee0ad6be26..2ae253aae4bd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/event/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/event/package-info.java @@ -1,9 +1,7 @@ /** * Test event support classes for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.event; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/hint/TestContextRuntimeHints.java b/spring-test/src/main/java/org/springframework/test/context/hint/TestContextRuntimeHints.java index db1ec2797702..54329ee89d71 100644 --- a/spring-test/src/main/java/org/springframework/test/context/hint/TestContextRuntimeHints.java +++ b/spring-test/src/main/java/org/springframework/test/context/hint/TestContextRuntimeHints.java @@ -20,13 +20,14 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.aot.hint.MemberCategory; import org.springframework.aot.hint.ReflectionHints; import org.springframework.aot.hint.RuntimeHints; import org.springframework.aot.hint.RuntimeHintsRegistrar; import org.springframework.aot.hint.TypeHint; import org.springframework.aot.hint.TypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/hint/package-info.java b/spring-test/src/main/java/org/springframework/test/context/hint/package-info.java index 1043d98cead9..c441130bd049 100644 --- a/spring-test/src/main/java/org/springframework/test/context/hint/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/hint/package-info.java @@ -2,9 +2,7 @@ * Support for registering hints for reflection and resources in the * Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.hint; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java index 3e49b71b39cb..bda7712318e1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/MergedSqlConfig.java @@ -19,13 +19,14 @@ import java.lang.reflect.Array; import java.util.Arrays; +import org.jspecify.annotations.Nullable; + import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; import org.springframework.jdbc.datasource.init.ScriptUtils; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.jdbc.SqlConfig.ErrorMode; import org.springframework.test.context.jdbc.SqlConfig.TransactionMode; 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 c42cb28bfcf3..5446fef59d9a 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 @@ * {@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 75f8aa36c5da..1b3f8f19fb1f 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 @@ *

    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 6479a85524e4..ad997a4cc90d 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 @@ *

    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/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index 1a8cb23bf06c..0bf31964b2e0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -26,6 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aot.hint.RuntimeHints; import org.springframework.context.ApplicationContext; @@ -35,7 +36,6 @@ import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.Resource; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.aot.AotTestExecutionListener; @@ -249,16 +249,14 @@ private boolean mergeSqlAnnotations(TestContext testContext) { /** * Get the {@code @SqlMergeMode} annotation declared on the supplied class. */ - @Nullable - private SqlMergeMode getSqlMergeModeFor(Class clazz) { + private @Nullable SqlMergeMode getSqlMergeModeFor(Class clazz) { return TestContextAnnotationUtils.findMergedAnnotation(clazz, SqlMergeMode.class); } /** * Get the {@code @SqlMergeMode} annotation declared on the supplied method. */ - @Nullable - private SqlMergeMode getSqlMergeModeFor(Method method) { + private @Nullable SqlMergeMode getSqlMergeModeFor(Method method) { return AnnotatedElementUtils.findMergedAnnotation(method, SqlMergeMode.class); } @@ -397,8 +395,7 @@ private static boolean sameDataSource(DataSource ds1, DataSource ds2) { .equals(TransactionSynchronizationUtils.unwrapResourceIfNecessary(ds2)); } - @Nullable - private DataSource getDataSourceFromTransactionManager(PlatformTransactionManager transactionManager) { + private @Nullable DataSource getDataSourceFromTransactionManager(PlatformTransactionManager transactionManager) { try { Method getDataSourceMethod = transactionManager.getClass().getMethod("getDataSource"); Object obj = ReflectionUtils.invokeMethod(getDataSourceMethod, transactionManager); @@ -424,7 +421,7 @@ private String[] getScripts(Sql sql, Class testClass, @Nullable Method testMe * Detect a default SQL script by implementing the algorithm defined in * {@link Sql#scripts}. */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private String detectDefaultScript(Class testClass, @Nullable Method testMethod, boolean classLevel) { Assert.state(classLevel || testMethod != null, "Method-level @Sql requires a testMethod"); diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/package-info.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/package-info.java index fe01ea1dc52b..f5158c330957 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/package-info.java @@ -2,9 +2,7 @@ * JDBC support classes for the Spring TestContext Framework, * including support for declarative SQL script execution via {@code @Sql}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.jdbc; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java index c93f477b5555..2e48fbc561f1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit/jupiter/AbstractExpressionEvaluatingCondition.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. @@ -118,8 +118,8 @@ protected ConditionEvaluationResult evaluateAnnotation(Cl if (logger.isInfoEnabled()) { logger.info(reason); } - result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason) - : ConditionEvaluationResult.disabled(reason)); + result = (enabledOnTrue ? ConditionEvaluationResult.enabled(reason) : + ConditionEvaluationResult.disabled(reason)); } else { String adjective = (enabledOnTrue ? "disabled" : "enabled"); 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 02223b656643..85adb729c0af 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 @@ * {@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 6c4908eb8bf9..3549c3e63de3 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 @@ /** * {@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 a80f2cef8095..47c8364ee53f 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 @@ * {@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 f637c05c52c6..5b7fe8257c4d 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 @@ /** * {@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 5e66aa58f4c8..820725d7e45a 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. @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; @@ -50,7 +51,6 @@ import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.RepeatableContainers; -import org.springframework.lang.Nullable; import org.springframework.test.context.MethodInvoker; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestContextAnnotationUtils; @@ -65,7 +65,7 @@ /** * {@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 void afterAll(ExtensionContext context) throws Exception { /** * 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 { @@ -330,8 +329,7 @@ private boolean supportsApplicationEvents(ParameterContext parameterContext) { * @see ParameterResolutionDelegate#resolveDependency */ @Override - @Nullable - public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { + public @Nullable Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext) { Parameter parameter = parameterContext.getParameter(); int index = parameterContext.getIndex(); Class testClass = extensionContext.getRequiredTestClass(); @@ -374,7 +372,7 @@ private static Store getStore(ExtensionContext context) { * the supplied {@link TestContextManager}. * @since 6.1 */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // org.junit.jupiter.api.extension.ExecutableInvoker is not null marked private static void registerMethodInvoker(TestContextManager testContextManager, ExtensionContext context) { testContextManager.getTestContext().setMethodInvoker(context.getExecutableInvoker()::invoke); } 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 f493ad5b2cd8..8d311e438f45 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 @@ * {@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 c9c4800b6a12..43456896139a 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,10 +1,8 @@ /** * Core support for integrating the Spring TestContext Framework - * with the JUnit Jupiter extension model in JUnit 5. + * with the JUnit Jupiter testing framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.junit.jupiter; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 595695ee8926..b42dbd30ed36 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 @@ * {@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 2f48593acb46..159f3b5e393e 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,10 +1,8 @@ /** * Web support for integrating the Spring TestContext Framework - * with the JUnit Jupiter extension model in JUnit 5. + * with the JUnit Jupiter extension model. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.junit.jupiter.web; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 9c39288080b3..a873d57e335d 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. @@ -18,11 +18,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.junit.runner.RunWith; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextManager; @@ -62,8 +62,12 @@ * @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 { /** @@ -75,8 +79,7 @@ public abstract class AbstractJUnit4SpringContextTests implements ApplicationCon * The {@link ApplicationContext} that was injected into this test instance * via {@link #setApplicationContext(ApplicationContext)}. */ - @Nullable - protected ApplicationContext applicationContext; + protected @Nullable ApplicationContext applicationContext; /** 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 24e3738254b2..6d1e5a688d9a 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. @@ -18,13 +18,14 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestExecutionListeners; import org.springframework.test.jdbc.JdbcTestUtils; @@ -77,8 +78,12 @@ * @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 { /** @@ -87,8 +92,7 @@ public abstract class AbstractTransactionalJUnit4SpringContextTests extends Abst */ protected final JdbcTemplate jdbcTemplate = new JdbcTemplate(); - @Nullable - private String sqlScriptEncoding; + private @Nullable String sqlScriptEncoding; /** 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 d06106d74693..c8bb57dd6d8c 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. @@ -22,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.junit.Ignore; import org.junit.Test; import org.junit.internal.runners.model.ReflectiveCallable; @@ -35,7 +36,6 @@ import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.ProfileValueUtils; import org.springframework.test.annotation.TestAnnotationUtils; import org.springframework.test.context.TestContextManager; @@ -93,7 +93,11 @@ * @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); @@ -346,8 +350,7 @@ protected Statement possiblyExpectingExceptions(FrameworkMethod frameworkMethod, *

    Can be overridden by subclasses. * @return the expected exception, or {@code null} if none was specified */ - @Nullable - protected Class getExpectedException(FrameworkMethod frameworkMethod) { + protected @Nullable Class getExpectedException(FrameworkMethod frameworkMethod) { Test test = frameworkMethod.getAnnotation(Test.class); return (test != null && test.expected() != Test.None.class ? test.expected() : null); } 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 dd6ca659bc91..97bfd42b4a66 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 @@ * @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/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java index b4b5883467f7..4013649ceadf 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java @@ -2,9 +2,7 @@ * Support classes for integrating the Spring TestContext Framework * with JUnit 4.12 or higher. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.junit4; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 f2c5f31dce93..d0da5972dfa1 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 @@ * @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 bb205c7cd82c..2ebacc0bc4a4 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 @@ * @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/rules/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/package-info.java index d1d981d2fb48..0d4dfecd907d 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/package-info.java @@ -1,9 +1,7 @@ /** * Custom JUnit 4 {@code Rules} used in the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.junit4.rules; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 d9162ae0d4d6..7da5c29e8d1d 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. @@ -19,11 +19,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.Method; +import org.jspecify.annotations.Nullable; import org.junit.AssumptionViolatedException; import org.junit.runners.model.Statement; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.annotation.ProfileValueUtils; import org.springframework.util.Assert; @@ -39,15 +39,18 @@ * @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; private final Class testClass; - @Nullable - private final Method testMethod; + private final @Nullable Method testMethod; /** 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 467b2f708169..c39615219064 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 @@ * @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 f166cd26a06b..8e4b67961d43 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 @@ * @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 db8b747b97bb..687c027d97c9 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 @@ * @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 afadf43a8898..1979d2d9c9ad 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 @@ * @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 f95eb5a3823e..3f260a92e759 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 @@ * @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 7e5c882a258a..08bd8f512e87 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 @@ * @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 12baf0ea7772..d041ccfde989 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 @@ * @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 cb4e1dc6d9cf..88d70034c28d 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 @@ * @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 b7277f364dec..9adce8646265 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 @@ * @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/junit4/statements/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java index 50b9815c18fa..6733a69c4ccd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/package-info.java @@ -1,9 +1,7 @@ /** * Custom JUnit 4 {@code Statements} used in the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.junit4.statements; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationRegistryTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationRegistryTestExecutionListener.java index 8178d825c432..974dc8dd6751 100644 --- a/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationRegistryTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/observation/MicrometerObservationRegistryTestExecutionListener.java @@ -22,10 +22,10 @@ import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.core.Conventions; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.util.ReflectionUtils; @@ -74,8 +74,7 @@ class MicrometerObservationRegistryTestExecutionListener extends AbstractTestExe static final String OBSERVATION_THREAD_LOCAL_ACCESSOR_CLASS_NAME = "io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor"; - @Nullable - private static final String ERROR_MESSAGE; + private static final @Nullable String ERROR_MESSAGE; static { // Trigger eager resolution of Micrometer Observation types to ensure that diff --git a/spring-test/src/main/java/org/springframework/test/context/observation/package-info.java b/spring-test/src/main/java/org/springframework/test/context/observation/package-info.java index fecd9bf754ed..9a43008f5659 100644 --- a/spring-test/src/main/java/org/springframework/test/context/observation/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/observation/package-info.java @@ -1,9 +1,7 @@ /** * Observation support classes for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.observation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/package-info.java b/spring-test/src/main/java/org/springframework/test/context/package-info.java index af3710b4b2dc..e5bf853860b1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/package-info.java @@ -11,9 +11,7 @@ * and caching, dependency injection of test fixtures, and transactional test * management with default rollback semantics. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java index 084d6ae7fffc..24407401e200 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDirtiesContextTestExecutionListener.java @@ -20,10 +20,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.ApplicationContext; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext.ClassMode; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; @@ -84,7 +84,7 @@ protected void dirtyContext(TestContext testContext, @Nullable HierarchyMode hie * @since 4.2 * @see #dirtyContext */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void beforeOrAfterTestMethod(TestContext testContext, MethodMode requiredMethodMode, ClassMode requiredClassMode) throws Exception { @@ -136,7 +136,7 @@ else if (logger.isDebugEnabled()) { * @since 4.2 * @see #dirtyContext */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation protected void beforeOrAfterTestClass(TestContext testContext, ClassMode requiredClassMode) throws Exception { Assert.notNull(testContext, "TestContext must not be null"); Assert.notNull(requiredClassMode, "requiredClassMode must not be null"); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index df48cd626f2e..5a11d1cff04f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -26,11 +26,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotationAwareOrderComparator; -import org.springframework.lang.Nullable; import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.ContextConfiguration; @@ -79,8 +79,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot private final Log logger = LogFactory.getLog(getClass()); - @Nullable - private BootstrapContext bootstrapContext; + private @Nullable BootstrapContext bootstrapContext; @Override @@ -533,8 +532,7 @@ protected ContextLoader resolveContextLoader(Class testClass, * @throws IllegalArgumentException if supplied configuration attributes are * {@code null} or empty */ - @Nullable - protected Class resolveExplicitContextLoaderClass( + protected @Nullable Class resolveExplicitContextLoaderClass( List configAttributesList) { Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null"); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java index a531d0c44786..3a692987de3f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java @@ -22,10 +22,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.test.context.SmartContextLoader; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java index d768a8cfaf2e..56527d7cf9b2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -232,7 +232,7 @@ static Map> buildContextHierarchyMa * @throws IllegalArgumentException if the supplied class is {@code null} or if * {@code @ContextConfiguration} is not present on the supplied class */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation static List resolveContextConfigurationAttributes(Class testClass) { Assert.notNull(testClass, "Class must not be null"); @@ -252,8 +252,8 @@ static List resolveContextConfigurationAttribute // annotated class. if (currentAnnotation.equals(previousAnnotation) && hasResources(currentAnnotation)) { if (logger.isDebugEnabled()) { - logger.debug(String.format("Ignoring duplicate %s declaration on [%s], " - + "since it is also declared on [%s].", currentAnnotation, + logger.debug(String.format("Ignoring duplicate %s declaration on [%s], " + + "since it is also declared on [%s].", currentAnnotation, previousDeclaringClass.getName(), descriptor.getRootDeclaringClass().getName())); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java index 03b6a25827ed..ae9fac01eb21 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DefaultTestContext.java @@ -21,12 +21,13 @@ import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.DirtiesContext.HierarchyMode; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.MergedContextConfiguration; @@ -56,14 +57,11 @@ public class DefaultTestContext implements TestContext { private final Class testClass; - @Nullable - private volatile Object testInstance; + private volatile @Nullable Object testInstance; - @Nullable - private volatile Method testMethod; + private volatile @Nullable Method testMethod; - @Nullable - private volatile Throwable testException; + private volatile @Nullable Throwable testException; private volatile MethodInvoker methodInvoker = MethodInvoker.DEFAULT_INVOKER; @@ -174,8 +172,7 @@ public final Method getTestMethod() { } @Override - @Nullable - public final Throwable getTestException() { + public final @Nullable Throwable getTestException() { return this.testException; } @@ -211,8 +208,7 @@ public void setAttribute(String name, @Nullable Object value) { } @Override - @Nullable - public Object getAttribute(String name) { + public @Nullable Object getAttribute(String name) { Assert.notNull(name, "Name must not be null"); return this.attributes.get(name); } @@ -229,8 +225,7 @@ public T computeAttribute(String name, Function computeFunction) } @Override - @Nullable - public Object removeAttribute(String name) { + public @Nullable Object removeAttribute(String name) { Assert.notNull(name, "Name must not be null"); return this.attributes.remove(name); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java index 26a4cad9a31c..9773943a2eda 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DelegatingSmartContextLoader.java @@ -36,12 +36,14 @@ */ public class DelegatingSmartContextLoader extends AbstractDelegatingSmartContextLoader { - private static final String GROOVY_XML_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.support.GenericGroovyXmlContextLoader"; + private static final String GROOVY_XML_CONTEXT_LOADER_CLASS_NAME = + "org.springframework.test.context.support.GenericGroovyXmlContextLoader"; private static final boolean groovyPresent = ClassUtils.isPresent("groovy.lang.Closure", - DelegatingSmartContextLoader.class.getClassLoader()) - && ClassUtils.isPresent(GROOVY_XML_CONTEXT_LOADER_CLASS_NAME, - DelegatingSmartContextLoader.class.getClassLoader()); + DelegatingSmartContextLoader.class.getClassLoader()) && + ClassUtils.isPresent(GROOVY_XML_CONTEXT_LOADER_CLASS_NAME, + DelegatingSmartContextLoader.class.getClassLoader()); + private final SmartContextLoader xmlLoader; private final SmartContextLoader annotationConfigLoader; @@ -55,8 +57,8 @@ public DelegatingSmartContextLoader() { this.xmlLoader = (SmartContextLoader) BeanUtils.instantiateClass(loaderClass); } catch (Throwable ex) { - throw new IllegalStateException("Failed to enable support for Groovy scripts; " - + "could not load class: " + GROOVY_XML_CONTEXT_LOADER_CLASS_NAME, ex); + throw new IllegalStateException("Failed to enable support for Groovy scripts; " + + "could not load class: " + GROOVY_XML_CONTEXT_LOADER_CLASS_NAME, ex); } } else { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java index bc70c3606613..57254fd0548e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizer.java @@ -20,13 +20,14 @@ import java.lang.reflect.Modifier; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.beans.factory.support.BeanDefinitionRegistry; import org.springframework.beans.factory.support.RootBeanDefinition; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.env.ConfigurableEnvironment; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.MergedContextConfiguration; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java index ddd11ffe6a96..fae55eadc5fe 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertiesContextCustomizerFactory.java @@ -22,9 +22,10 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.core.MethodIntrospector; import org.springframework.core.annotation.MergedAnnotations; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.DynamicPropertySource; @@ -45,8 +46,7 @@ class DynamicPropertiesContextCustomizerFactory implements ContextCustomizerFactory { @Override - @Nullable - public DynamicPropertiesContextCustomizer createContextCustomizer(Class testClass, + public @Nullable DynamicPropertiesContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { Set methods = new LinkedHashSet<>(); diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer.java index 3f24c6dd8a22..5946f0590c50 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer.java @@ -18,6 +18,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.BeanFactoryInitializer; import org.springframework.beans.factory.BeanFactoryUtils; @@ -25,7 +26,6 @@ import org.springframework.context.EnvironmentAware; import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.Environment; -import org.springframework.lang.Nullable; import org.springframework.test.context.DynamicPropertyRegistrar; import org.springframework.test.context.DynamicPropertyRegistry; @@ -49,8 +49,7 @@ public class DynamicPropertyRegistrarBeanInitializer implements BeanFactoryIniti "org.springframework.test.context.support.internalDynamicPropertyRegistrarBeanInitializer"; - @Nullable - private ConfigurableEnvironment environment; + private @Nullable ConfigurableEnvironment environment; @Override diff --git a/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java index bbe560152eaa..2a5470bdf26f 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/DynamicValuesPropertySource.java @@ -21,11 +21,12 @@ import java.util.Map; import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.core.env.ConfigurableEnvironment; import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.MutablePropertySources; import org.springframework.core.env.PropertySource; -import org.springframework.lang.Nullable; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.util.Assert; import org.springframework.util.function.SupplierUtils; @@ -60,8 +61,7 @@ class DynamicValuesPropertySource extends MapPropertySource { @Override - @Nullable - public Object getProperty(String name) { + public @Nullable Object getProperty(String name) { return SupplierUtils.resolve(super.getProperty(name)); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java b/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java index ba76d8f19175..0501c9e30a33 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/MergedTestPropertySources.java @@ -19,11 +19,12 @@ import java.util.Arrays; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestPropertySource; import org.springframework.util.Assert; diff --git a/spring-test/src/main/java/org/springframework/test/context/support/PropertyProvider.java b/spring-test/src/main/java/org/springframework/test/context/support/PropertyProvider.java index 564ed078a936..a1d6367d8d15 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/PropertyProvider.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/PropertyProvider.java @@ -16,7 +16,7 @@ package org.springframework.test.context.support; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Strategy for providing named properties — for example, for looking up @@ -35,7 +35,6 @@ public interface PropertyProvider { * @param name the name of the property to retrieve * @return the value of the property or {@code null} if not found */ - @Nullable - String get(String name); + @Nullable String get(String name); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java index b2dfa3707545..cbf8bdd06062 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestConstructorUtils.java @@ -23,11 +23,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.SpringProperties; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestConstructor; import org.springframework.test.context.TestConstructor.AutowireMode; import org.springframework.test.context.TestContextAnnotationUtils; @@ -63,15 +63,6 @@ public abstract class TestConstructorUtils { catch (ClassNotFoundException ex) { // jakarta.inject API not available - simply skip. } - - try { - autowiredAnnotationTypes.add((Class) - ClassUtils.forName("javax.inject.Inject", classLoader)); - logger.trace("'javax.inject.Inject' annotation found and supported for autowiring"); - } - catch (ClassNotFoundException ex) { - // javax.inject API not available - simply skip. - } } @@ -135,9 +126,8 @@ public static boolean isAutowirableConstructor(Executable executable, Class t * conditions is {@code true}. * *

      - *
    1. The constructor is annotated with {@link Autowired @Autowired}, - * {@link jakarta.inject.Inject @jakarta.inject.Inject}, or - * {@link javax.inject.Inject @javax.inject.Inject}.
    2. + *
    3. The constructor is annotated with {@link Autowired @Autowired} or + * {@link jakarta.inject.Inject @jakarta.inject.Inject}.
    4. *
    5. {@link TestConstructor @TestConstructor} is present or * meta-present on the test class with * {@link TestConstructor#autowireMode() autowireMode} set to diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java index c3a43e594dc8..a9e0470097bc 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceAttributes.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,6 +22,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.core.annotation.MergedAnnotation; import org.springframework.core.io.ClassPathResource; @@ -31,7 +32,6 @@ import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.util.TestContextResourceUtils; import org.springframework.util.Assert; @@ -92,9 +92,9 @@ class TestPropertySourceAttributes { */ void mergeWith(TestPropertySourceAttributes attributes) { Assert.state(attributes.declaringClass == this.declaringClass, - () -> "Detected @TestPropertySource declarations within an aggregate index " - + "with different sources: " + this.declaringClass.getName() + " and " - + attributes.declaringClass.getName()); + () -> "Detected @TestPropertySource declarations within an aggregate index " + + "with different sources: " + this.declaringClass.getName() + " and " + + attributes.declaringClass.getName()); logger.trace(LogMessage.format("Retrieved %s for declaring class [%s].", attributes, this.declaringClass.getName())); assertSameBooleanAttribute(this.inheritLocations, attributes.inheritLocations, diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 700f38acd868..bd1dbc967451 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -29,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.context.ConfigurableApplicationContext; @@ -49,7 +50,6 @@ import org.springframework.core.io.support.PropertySourceFactory; import org.springframework.core.io.support.ResourcePatternResolver; import org.springframework.core.io.support.ResourcePatternUtils; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContextAnnotationUtils; import org.springframework.test.context.TestPropertySource; import org.springframework.util.Assert; @@ -116,8 +116,7 @@ static MergedTestPropertySources buildMergedTestPropertySources(Class testCla return new MergedTestPropertySources(mergeLocations(attributesList), mergeProperties(attributesList)); } - @Nullable - private static TestPropertySourceAttributes mergeTestPropertySourceAttributes( + private static @Nullable TestPropertySourceAttributes mergeTestPropertySourceAttributes( List aggregatedAttributesList) { TestPropertySourceAttributes mergedAttributes = null; @@ -135,7 +134,7 @@ else if (!duplicationDetected(currentAttributes, previousAttributes)) { return mergedAttributes; } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private static boolean duplicationDetected(TestPropertySourceAttributes currentAttributes, @Nullable TestPropertySourceAttributes previousAttributes) { @@ -460,8 +459,7 @@ private static class SequencedProperties extends Properties { private final LinkedHashMap map = new LinkedHashMap<>(); @Override - @Nullable - public Object put(Object key, Object value) { + public @Nullable Object put(Object key, Object value) { if (key instanceof String str) { return this.map.put(str, value); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/package-info.java b/spring-test/src/main/java/org/springframework/test/context/support/package-info.java index 66f278fde8a9..227ccd0446e8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.support; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java index c328f59a96b9..606932c04857 100644 --- a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTestNGSpringContextTests.java @@ -21,6 +21,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.testng.IHookCallBack; import org.testng.IHookable; import org.testng.ITestResult; @@ -31,7 +32,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestContext; import org.springframework.test.context.TestContextManager; @@ -69,13 +69,11 @@ public abstract class AbstractTestNGSpringContextTests implements IHookable, App * The {@link ApplicationContext} that was injected into this test instance * via {@link #setApplicationContext(ApplicationContext)}. */ - @Nullable - protected ApplicationContext applicationContext; + protected @Nullable ApplicationContext applicationContext; private final TestContextManager testContextManager; - @Nullable - private Throwable testException; + private @Nullable Throwable testException; /** @@ -198,8 +196,7 @@ protected void springTestContextAfterTestClass() throws Exception { } - @Nullable - private Throwable getTestResultException(ITestResult testResult) { + private @Nullable Throwable getTestResultException(ITestResult testResult) { Throwable testResultException = testResult.getThrowable(); if (testResultException instanceof InvocationTargetException) { testResultException = testResultException.getCause(); diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java index d26ec301d0ba..ba01dec09c09 100644 --- a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java @@ -18,13 +18,14 @@ import javax.sql.DataSource; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.ApplicationContext; import org.springframework.core.io.Resource; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator; -import org.springframework.lang.Nullable; import org.springframework.test.jdbc.JdbcTestUtils; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.annotation.Transactional; @@ -70,8 +71,7 @@ public abstract class AbstractTransactionalTestNGSpringContextTests extends Abst */ protected final JdbcTemplate jdbcTemplate = new JdbcTemplate(); - @Nullable - private String sqlScriptEncoding; + private @Nullable String sqlScriptEncoding; /** diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java b/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java index a3ffed50e190..fedbcb4ced32 100644 --- a/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/testng/package-info.java @@ -2,9 +2,7 @@ * Support classes for integrating the Spring TestContext Framework * with TestNG. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.testng; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java index 01c55a153509..ac7349637d26 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java @@ -22,12 +22,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.beans.factory.ListableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionManager; @@ -88,8 +88,7 @@ public abstract class TestContextTransactionUtils { * @throws BeansException if an error occurs while retrieving an explicitly * named {@code DataSource} */ - @Nullable - public static DataSource retrieveDataSource(TestContext testContext, @Nullable String name) { + public static @Nullable DataSource retrieveDataSource(TestContext testContext, @Nullable String name) { Assert.notNull(testContext, "TestContext must not be null"); BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); @@ -160,8 +159,7 @@ public static DataSource retrieveDataSource(TestContext testContext, @Nullable S * @throws IllegalStateException if more than one TransactionManagementConfigurer * exists in the ApplicationContext */ - @Nullable - public static PlatformTransactionManager retrieveTransactionManager(TestContext testContext, @Nullable String name) { + public static @Nullable PlatformTransactionManager retrieveTransactionManager(TestContext testContext, @Nullable String name) { Assert.notNull(testContext, "TestContext must not be null"); BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); @@ -277,8 +275,7 @@ public TestContextTransactionAttribute( } @Override - @Nullable - public String getName() { + public @Nullable String getName() { return this.name; } } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java index 73886f24129f..fde1518087b8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContext.java @@ -20,8 +20,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionDefinition; @@ -52,8 +52,7 @@ class TransactionContext { private boolean flaggedForRollback; - @Nullable - private TransactionStatus transactionStatus; + private @Nullable TransactionStatus transactionStatus; private final AtomicInteger transactionsStarted = new AtomicInteger(); @@ -69,8 +68,7 @@ class TransactionContext { } - @Nullable - TransactionStatus getTransactionStatus() { + @Nullable TransactionStatus getTransactionStatus() { return this.transactionStatus; } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java index 3d1a1bcbcc57..91ea7504d2c7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionContextHolder.java @@ -16,8 +16,9 @@ package org.springframework.test.context.transaction; +import org.jspecify.annotations.Nullable; + import org.springframework.core.NamedInheritableThreadLocal; -import org.springframework.lang.Nullable; /** * {@link InheritableThreadLocal}-based holder for the current {@link TransactionContext}. @@ -39,13 +40,11 @@ static void setCurrentTransactionContext(TransactionContext transactionContext) currentTransactionContext.set(transactionContext); } - @Nullable - static TransactionContext getCurrentTransactionContext() { + static @Nullable TransactionContext getCurrentTransactionContext() { return currentTransactionContext.get(); } - @Nullable - static TransactionContext removeCurrentTransactionContext() { + static @Nullable TransactionContext removeCurrentTransactionContext() { TransactionContext transactionContext = currentTransactionContext.get(); currentTransactionContext.remove(); return transactionContext; diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 279e1b6b8b08..85c3b73ccd75 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -25,12 +25,12 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.lang.Nullable; import org.springframework.test.annotation.Commit; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.TestContext; @@ -161,8 +161,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false) { @Override - @Nullable - protected TransactionAttribute findTransactionAttribute(Class clazz) { + protected @Nullable TransactionAttribute findTransactionAttribute(Class clazz) { // @Transactional present in inheritance hierarchy? TransactionAttribute result = super.findTransactionAttribute(clazz); if (result != null) { @@ -172,8 +171,7 @@ protected TransactionAttribute findTransactionAttribute(Class clazz) { return findTransactionAttributeInEnclosingClassHierarchy(clazz); } - @Nullable - private TransactionAttribute findTransactionAttributeInEnclosingClassHierarchy(Class clazz) { + private @Nullable TransactionAttribute findTransactionAttributeInEnclosingClassHierarchy(Class clazz) { if (TestContextAnnotationUtils.searchEnclosingClass(clazz)) { return findTransactionAttribute(clazz.getEnclosingClass()); } @@ -207,7 +205,7 @@ public final int getOrder() { * @see #getTransactionManager(TestContext, String) */ @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public void beforeTestMethod(final TestContext testContext) throws Exception { Method testMethod = testContext.getTestMethod(); Class testClass = testContext.getTestClass(); @@ -372,8 +370,7 @@ else if (logger.isDebugEnabled()) { * @throws BeansException if an error occurs while retrieving the transaction manager * @see #getTransactionManager(TestContext) */ - @Nullable - protected PlatformTransactionManager getTransactionManager(TestContext testContext, @Nullable String qualifier) { + protected @Nullable PlatformTransactionManager getTransactionManager(TestContext testContext, @Nullable String qualifier) { // Look up by type and qualifier from @Transactional if (StringUtils.hasText(qualifier)) { try { @@ -411,8 +408,7 @@ protected PlatformTransactionManager getTransactionManager(TestContext testConte * exists in the ApplicationContext * @see #getTransactionManager(TestContext, String) */ - @Nullable - protected PlatformTransactionManager getTransactionManager(TestContext testContext) { + protected @Nullable PlatformTransactionManager getTransactionManager(TestContext testContext) { return TestContextTransactionUtils.retrieveTransactionManager(testContext, null); } diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java b/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java index 51a8296e4527..3cb1ecb28326 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/package-info.java @@ -1,9 +1,7 @@ /** * Transactional support classes for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.transaction; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/util/package-info.java b/spring-test/src/main/java/org/springframework/test/context/util/package-info.java index f4addd2e9e50..2875ad70df6c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/util/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/util/package-info.java @@ -1,9 +1,7 @@ /** * Common utilities used within the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.util; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 52cc39e6b708..5478fbc7b2b3 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 @@ *

      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/web/WebDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java index 7298a632ff4d..9973298602c7 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebDelegatingSmartContextLoader.java @@ -39,9 +39,9 @@ public class WebDelegatingSmartContextLoader extends AbstractDelegatingSmartCont private static final String GROOVY_XML_WEB_CONTEXT_LOADER_CLASS_NAME = "org.springframework.test.context.web.GenericGroovyXmlWebContextLoader"; private static final boolean groovyPresent = ClassUtils.isPresent("groovy.lang.Closure", - WebDelegatingSmartContextLoader.class.getClassLoader()) - && ClassUtils.isPresent(GROOVY_XML_WEB_CONTEXT_LOADER_CLASS_NAME, - WebDelegatingSmartContextLoader.class.getClassLoader()); + WebDelegatingSmartContextLoader.class.getClassLoader()) && + ClassUtils.isPresent(GROOVY_XML_WEB_CONTEXT_LOADER_CLASS_NAME, + WebDelegatingSmartContextLoader.class.getClassLoader()); private final SmartContextLoader xmlLoader; private final SmartContextLoader annotationConfigLoader; @@ -55,8 +55,8 @@ public WebDelegatingSmartContextLoader() { this.xmlLoader = (SmartContextLoader) BeanUtils.instantiateClass(loaderClass); } catch (Throwable ex) { - throw new IllegalStateException("Failed to enable support for Groovy scripts; " - + "could not load class: " + GROOVY_XML_WEB_CONTEXT_LOADER_CLASS_NAME, ex); + throw new IllegalStateException("Failed to enable support for Groovy scripts; " + + "could not load class: " + GROOVY_XML_WEB_CONTEXT_LOADER_CLASS_NAME, ex); } } else { diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java index 79c948a30b4a..4443f6a554df 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,12 +19,13 @@ import java.util.List; import java.util.Set; +import org.jspecify.annotations.Nullable; + import org.springframework.context.ApplicationContextInitializer; import org.springframework.core.io.support.PropertySourceDescriptor; import org.springframework.core.style.DefaultToStringStyler; import org.springframework.core.style.SimpleValueStyler; import org.springframework.core.style.ToStringCreator; -import org.springframework.lang.Nullable; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextLoader; @@ -105,9 +106,9 @@ public WebMergedContextConfiguration(MergedContextConfiguration mergedConfig, St * {@link #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ @Deprecated(since = "6.1") - public WebMergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public WebMergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @Nullable String[] propertySourceProperties, + String @Nullable [] activeProfiles, String @Nullable [] propertySourceLocations, String @Nullable [] propertySourceProperties, String resourceBasePath, ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { @@ -144,9 +145,9 @@ public WebMergedContextConfiguration(Class testClass, @Nullable String[] loca * {@link #WebMergedContextConfiguration(Class, String[], Class[], Set, String[], List, String[], Set, String, ContextLoader, CacheAwareContextLoaderDelegate, MergedContextConfiguration)} */ @Deprecated(since = "6.1") - public WebMergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public WebMergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, @Nullable String[] propertySourceLocations, @Nullable String[] propertySourceProperties, + String @Nullable [] activeProfiles, String @Nullable [] propertySourceLocations, String @Nullable [] propertySourceProperties, @Nullable Set contextCustomizers, String resourceBasePath, ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { @@ -180,10 +181,10 @@ public WebMergedContextConfiguration(Class testClass, @Nullable String[] loca * @param parent the parent configuration or {@code null} if there is no parent * @since 6.1 */ - public WebMergedContextConfiguration(Class testClass, @Nullable String[] locations, @Nullable Class[] classes, + public WebMergedContextConfiguration(Class testClass, String @Nullable [] locations, Class @Nullable [] classes, @Nullable Set>> contextInitializerClasses, - @Nullable String[] activeProfiles, - List propertySourceDescriptors, @Nullable String[] propertySourceProperties, + String @Nullable [] activeProfiles, + List propertySourceDescriptors, String @Nullable [] propertySourceProperties, @Nullable Set contextCustomizers, String resourceBasePath, ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, @Nullable MergedContextConfiguration parent) { diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java index 6db3a3351dea..ec39473b46db 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java @@ -16,7 +16,8 @@ package org.springframework.test.context.web; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; @@ -71,8 +72,7 @@ protected MergedContextConfiguration processMergedContextConfiguration(MergedCon } } - @Nullable - private static WebAppConfiguration getWebAppConfiguration(Class testClass) { + private static @Nullable WebAppConfiguration getWebAppConfiguration(Class testClass) { return TestContextAnnotationUtils.findMergedAnnotation(testClass, WebAppConfiguration.class); } diff --git a/spring-test/src/main/java/org/springframework/test/context/web/package-info.java b/spring-test/src/main/java/org/springframework/test/context/web/package-info.java index e4f9b40942b2..82d61b9f0e37 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/package-info.java @@ -1,9 +1,7 @@ /** * Web support classes for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.web; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java index 441615cf1c68..67b6298ad393 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java @@ -17,9 +17,9 @@ package org.springframework.test.context.web.socket; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.lang.Nullable; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.web.context.WebApplicationContext; diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java index 667b789f8bc6..fee36dd3bdeb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java @@ -18,7 +18,8 @@ import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextCustomizerFactory; @@ -42,8 +43,7 @@ class MockServerContainerContextCustomizerFactory implements ContextCustomizerFa @Override - @Nullable - public ContextCustomizer createContextCustomizer(Class testClass, + public @Nullable ContextCustomizer createContextCustomizer(Class testClass, List configAttributes) { if (webSocketPresent && isAnnotatedWithWebAppConfiguration(testClass)) { diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java index 8aa1072223a6..f3401796d767 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java @@ -1,9 +1,7 @@ /** * WebSocket support classes for the Spring TestContext Framework. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.web.socket; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java index ec6d984dc85e..c2d3cc863659 100644 --- a/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java +++ b/spring-test/src/main/java/org/springframework/test/http/HttpHeadersAssert.java @@ -20,10 +20,15 @@ import java.time.ZoneId; import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; +import java.util.Arrays; +import java.util.Collection; import java.util.List; +import java.util.function.Consumer; -import org.assertj.core.api.AbstractMapAssert; +import org.assertj.core.api.AbstractCollectionAssert; +import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; +import org.assertj.core.api.ObjectAssert; import org.springframework.http.HttpHeaders; @@ -32,46 +37,72 @@ * {@link HttpHeaders}. * * @author Stephane Nicoll + * @author Simon Baslé * @since 6.2 */ -public class HttpHeadersAssert extends AbstractMapAssert> { +public class HttpHeadersAssert extends AbstractObjectAssert { private static final ZoneId GMT = ZoneId.of("GMT"); + private final AbstractCollectionAssert, String, ObjectAssert> namesAssert; + + public HttpHeadersAssert(HttpHeaders actual) { super(actual, HttpHeadersAssert.class); as("HTTP headers"); + this.namesAssert = Assertions.assertThat(actual.headerNames()) + .as("HTTP header names"); } + /** * Verify that the actual HTTP headers contain a header with the given * {@code name}. * @param name the name of an expected HTTP header - * @see #containsKey */ public HttpHeadersAssert containsHeader(String name) { - return containsKey(name); + this.namesAssert + .as("check headers contain HTTP header '%s'", name) + .contains(name); + return this.myself; } /** * Verify that the actual HTTP headers contain the headers with the given * {@code names}. * @param names the names of expected HTTP headers - * @see #containsKeys */ public HttpHeadersAssert containsHeaders(String... names) { - return containsKeys(names); + this.namesAssert + .as("check headers contain HTTP headers '%s'", Arrays.toString(names)) + .contains(names); + return this.myself; + } + + /** + * Verify that the actual HTTP headers contain only the headers with the + * given {@code names}, in any order and in a case-insensitive manner. + * @param names the names of expected HTTP headers + * @since 7.0 + */ + public HttpHeadersAssert containsOnlyHeaders(String... names) { + this.namesAssert + .as("check headers contain only HTTP headers '%s'", Arrays.toString(names)) + .containsOnly(names); + return this.myself; } /** * Verify that the actual HTTP headers do not contain a header with the * given {@code name}. * @param name the name of an HTTP header that should not be present - * @see #doesNotContainKey */ public HttpHeadersAssert doesNotContainHeader(String name) { - return doesNotContainKey(name); + this.namesAssert + .as("check headers do not contain HTTP header '%s'", name) + .doesNotContain(name); + return this.myself; } /** @@ -79,32 +110,39 @@ public HttpHeadersAssert doesNotContainHeader(String name) { * with the given {@code names}. * @param names the names of HTTP headers that should not be present * @since 6.2.2 - * @see #doesNotContainKeys */ public HttpHeadersAssert doesNotContainHeaders(String... names) { - return doesNotContainKeys(names); + this.namesAssert + .as("check headers do not contain HTTP headers '%s'", Arrays.toString(names)) + .doesNotContain(names); + return this.myself; } /** - * Verify that the actual HTTP headers do not contain any of the headers - * with the given {@code names}. - * @param names the names of HTTP headers that should not be present - * @see #doesNotContainKeys - * @deprecated in favor of {@link #doesNotContainHeaders(String...)} + * Verify that the actual HTTP headers contain a header with the given + * {@code name} that satisfies the given {@code valueRequirements}. + * @param name the name of the header + * @param valueRequirements the group of assertions to run against the + * values of the header with the given name + * @since 7.0 */ - @Deprecated(since = "6.2.2", forRemoval = true) - public HttpHeadersAssert doesNotContainsHeaders(String... names) { - return doesNotContainKeys(names); + @SuppressWarnings("unchecked") + public HttpHeadersAssert hasHeaderSatisfying(String name, Consumer> valueRequirements) { + containsHeader(name); + Assertions.assertThat(this.actual.get(name)) + .as("check all values for HTTP header '%s'", name) + .satisfies(values -> valueRequirements.accept((List) values)); + return this.myself; } /** * Verify that the actual HTTP headers contain a header with the given - * {@code name} and {@link String} {@code value}. + * {@code name} and {@link String} primary {@code value}. * @param name the name of the header * @param value the expected value of the header */ public HttpHeadersAssert hasValue(String name, String value) { - containsKey(name); + containsHeader(name); Assertions.assertThat(this.actual.getFirst(name)) .as("check primary value for HTTP header '%s'", name) .isEqualTo(value); @@ -113,12 +151,12 @@ public HttpHeadersAssert hasValue(String name, String value) { /** * Verify that the actual HTTP headers contain a header with the given - * {@code name} and {@code long} {@code value}. + * {@code name} and {@code long} primary {@code value}. * @param name the name of the header * @param value the expected value of the header */ public HttpHeadersAssert hasValue(String name, long value) { - containsKey(name); + containsHeader(name); Assertions.assertThat(this.actual.getFirst(name)) .as("check primary long value for HTTP header '%s'", name) .asLong().isEqualTo(value); @@ -127,16 +165,145 @@ public HttpHeadersAssert hasValue(String name, long value) { /** * Verify that the actual HTTP headers contain a header with the given - * {@code name} and {@link Instant} {@code value}. + * {@code name} and {@link Instant} primary {@code value}. * @param name the name of the header * @param value the expected value of the header */ public HttpHeadersAssert hasValue(String name, Instant value) { - containsKey(name); + containsHeader(name); Assertions.assertThat(this.actual.getFirstZonedDateTime(name)) .as("check primary date value for HTTP header '%s'", name) .isCloseTo(ZonedDateTime.ofInstant(value, GMT), Assertions.within(999, ChronoUnit.MILLIS)); return this.myself; } + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link String} primary {@code value}. + *

      This assertion fails if the header has secondary values. + * @param name the name of the header + * @param value the expected value of the header + * @since 7.0 + */ + public HttpHeadersAssert hasSingleValue(String name, String value) { + doesNotHaveSecondaryValues(name); + return hasValue(name, value); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@code long} primary {@code value}. + *

      This assertion fails if the header has secondary values. + * @param name the name of the header + * @param value the expected value of the header + * @since 7.0 + */ + public HttpHeadersAssert hasSingleValue(String name, long value) { + doesNotHaveSecondaryValues(name); + return hasValue(name, value); + } + + /** + * Verify that the actual HTTP headers contain a header with the given + * {@code name} and {@link Instant} primary {@code value}. + *

      This assertion fails if the header has secondary values. + * @param name the name of the header + * @param value the expected value of the header + * @since 7.0 + */ + public HttpHeadersAssert hasSingleValue(String name, Instant value) { + doesNotHaveSecondaryValues(name); + return hasValue(name, value); + } + + /** + * Verify that the given header has a full list of values exactly equal to + * the given list of values, and in the same order. + * @param name the considered header name (case-insensitive) + * @param values the exhaustive list of expected values + * @since 7.0 + */ + public HttpHeadersAssert hasExactlyValues(String name, List values) { + containsHeader(name); + Assertions.assertThat(this.actual.get(name)) + .as("check all values of HTTP header '%s'", name) + .containsExactlyElementsOf(values); + return this.myself; + } + + /** + * Verify that the given header has a full list of values exactly equal to + * the given list of values, in any order. + * @param name the considered header name (case-insensitive) + * @param values the exhaustive list of expected values + * @since 7.0 + */ + public HttpHeadersAssert hasExactlyValuesInAnyOrder(String name, List values) { + containsHeader(name); + Assertions.assertThat(this.actual.get(name)) + .as("check all values of HTTP header '%s' in any order", name) + .containsExactlyInAnyOrderElementsOf(values); + return this.myself; + } + + /** + * Verify that the actual HTTP headers are empty and no header is present. + */ + public HttpHeadersAssert isEmpty() { + this.namesAssert + .as("check headers are empty") + .isEmpty(); + return this.myself; + } + + /** + * Verify that the actual HTTP headers are not empty and at least one header + * is present. + */ + public HttpHeadersAssert isNotEmpty() { + this.namesAssert + .as("check headers are not empty") + .isNotEmpty(); + return this.myself; + } + + /** + * Verify that there are exactly {@code expected} headers present, when + * considering header names in a case-insensitive manner. + * @param expected the expected number of headers + */ + public HttpHeadersAssert hasSize(int expected) { + this.namesAssert + .as("check headers have size '%s'", expected) + .hasSize(expected); + return this.myself; + } + + /** + * Verify that the number of actual headers is the same as in the given + * {@code HttpHeaders}. + * @param other the {@code HttpHeaders} to compare size with + * @since 7.0 + */ + public HttpHeadersAssert hasSameSizeAs(HttpHeaders other) { + this.namesAssert + .as("check headers have same size as '%s'", other) + .hasSize(other.size()); + return this.myself; + } + + + private HttpHeadersAssert doesNotHaveSecondaryValues(String name) { + containsHeader(name); + List values = this.actual.get(name); + if (values != null && !values.isEmpty()) { + int size = values.size(); + Assertions.assertThat(size) + .withFailMessage("Expected HTTP header '%s' to be present " + + "without secondary values, but found <%s> secondary value(s)", name, size - 1) + .isOne(); + } + return this.myself; + } + } diff --git a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java index a25b39094d85..08f7c0972908 100644 --- a/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java +++ b/spring-test/src/main/java/org/springframework/test/http/MediaTypeAssert.java @@ -20,10 +20,10 @@ import org.assertj.core.api.Assertions; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.jspecify.annotations.Nullable; import org.springframework.http.InvalidMediaTypeException; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** @@ -103,7 +103,6 @@ public MediaTypeAssert isCompatibleWith(String mediaType) { } - @SuppressWarnings("NullAway") private MediaType parseMediaType(String value) { try { return MediaType.parseMediaType(value); diff --git a/spring-test/src/main/java/org/springframework/test/http/package-info.java b/spring-test/src/main/java/org/springframework/test/http/package-info.java index 6613b8a01284..0782d4904bf8 100644 --- a/spring-test/src/main/java/org/springframework/test/http/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/http/package-info.java @@ -1,9 +1,7 @@ /** * Test support for HTTP concepts. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.http; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java index 57d21d4422ba..4bfe45270a1a 100644 --- a/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java @@ -18,11 +18,11 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.jdbc.core.JdbcOperations; import org.springframework.jdbc.core.SqlParameterValue; import org.springframework.jdbc.core.simple.JdbcClient; -import org.springframework.lang.Nullable; import org.springframework.util.StringUtils; /** diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java b/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java index e382f904625f..7f2e3d9c89aa 100644 --- a/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/jdbc/package-info.java @@ -1,9 +1,7 @@ /** * Support classes for tests based on JDBC. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.jdbc; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java index e034682b598a..0d776b7bdf31 100644 --- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonContentAssert.java @@ -34,6 +34,7 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.core.io.ByteArrayResource; @@ -44,7 +45,6 @@ import org.springframework.http.HttpHeaders; import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.Assert; @@ -77,14 +77,11 @@ public abstract class AbstractJsonContentAssert resourceLoadClass; + private @Nullable Class resourceLoadClass; - @Nullable - private Charset charset; + private @Nullable Charset charset; private JsonLoader jsonLoader; @@ -488,12 +485,11 @@ public SELF withCharset(@Nullable Charset charset) { return this.myself; } - @Nullable - private String toJsonString() { + private @Nullable String toJsonString() { return (this.actual != null ? this.actual.getJson() : null); } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private String toNonNullJsonString() { String jsonString = toJsonString(); Assertions.assertThat(jsonString).as("JSON content").isNotNull(); @@ -546,8 +542,7 @@ private class JsonPathValue { this.jsonPath = JsonPath.compile(this.path); } - @Nullable - Object assertHasPath() { + @Nullable Object assertHasPath() { return getValue(); } @@ -560,8 +555,7 @@ void assertDoesNotHavePath() { } } - @Nullable - Object getValue() { + @Nullable Object getValue() { try { return read(); } @@ -570,8 +564,7 @@ Object getValue() { } } - @Nullable - private Object read() { + private @Nullable Object read() { return this.jsonPath.read(this.json); } diff --git a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java index ffa5409d9846..792b11abfc41 100644 --- a/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/AbstractJsonValueAssert.java @@ -31,10 +31,10 @@ import org.assertj.core.api.ObjectArrayAssert; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.http.converter.GenericHttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -64,8 +64,7 @@ public abstract class AbstractJsonValueAssert selfType, diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonAssert.java index a8d2bf404aca..926d2206b107 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonAssert.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,14 +17,13 @@ package org.springframework.test.json; import org.json.JSONException; +import org.jspecify.annotations.Nullable; import org.skyscreamer.jsonassert.JSONCompare; import org.skyscreamer.jsonassert.JSONCompareMode; import org.skyscreamer.jsonassert.JSONCompareResult; import org.skyscreamer.jsonassert.comparator.DefaultComparator; import org.skyscreamer.jsonassert.comparator.JSONComparator; -import org.springframework.lang.Nullable; - /** * Useful methods that can be used with {@code org.skyscreamer.jsonassert}. * @@ -41,8 +40,8 @@ public abstract class JsonAssert { * @see JSONCompareMode#LENIENT */ public static JsonComparator comparator(JsonCompareMode compareMode) { - JSONCompareMode jsonAssertCompareMode = (compareMode != JsonCompareMode.LENIENT - ? JSONCompareMode.STRICT : JSONCompareMode.LENIENT); + JSONCompareMode jsonAssertCompareMode = (compareMode != JsonCompareMode.LENIENT ? + JSONCompareMode.STRICT : JSONCompareMode.LENIENT); return comparator(jsonAssertCompareMode); } @@ -81,18 +80,14 @@ private static class JsonAssertJsonComparator implements JsonComparator { @Override public JsonComparison compare(@Nullable String expectedJson, @Nullable String actualJson) { if (actualJson == null) { - return (expectedJson != null) - ? JsonComparison.mismatch("Expected null JSON") - : JsonComparison.match(); + return (expectedJson != null) ? JsonComparison.mismatch("Expected null JSON") : JsonComparison.match(); } if (expectedJson == null) { return JsonComparison.mismatch("Expected non-null JSON"); } try { JSONCompareResult result = JSONCompare.compareJSON(expectedJson, actualJson, this.jsonAssertComparator); - return (!result.passed()) - ? JsonComparison.mismatch(result.getMessage()) - : JsonComparison.match(); + return (!result.passed()) ? JsonComparison.mismatch(result.getMessage()) : JsonComparison.match(); } catch (JSONException ex) { throw new IllegalStateException(ex); diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java b/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java index bc8dae2ee8ea..2fee94ef114f 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonComparator.java @@ -16,7 +16,8 @@ package org.springframework.test.json; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.json.JsonComparison.Result; /** diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java b/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java index 6930bc983335..eb2a60fe0f6f 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonComparison.java @@ -16,7 +16,7 @@ package org.springframework.test.json; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * A comparison of two JSON strings as returned from a {@link JsonComparator}. @@ -28,8 +28,7 @@ public final class JsonComparison { private final Result result; - @Nullable - private final String message; + private final @Nullable String message; private JsonComparison(Result result, @Nullable String message) { @@ -66,8 +65,7 @@ public Result getResult() { /** * Return a message describing the comparison. */ - @Nullable - public String getMessage() { + public @Nullable String getMessage() { return this.message; } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java index 28b02082e40b..7d3eba83f13d 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContent.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContent.java @@ -17,8 +17,8 @@ package org.springframework.test.json; import org.assertj.core.api.AssertProvider; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.util.Assert; @@ -34,8 +34,7 @@ public final class JsonContent implements AssertProvider { private final String json; - @Nullable - private final HttpMessageContentConverter contentConverter; + private final @Nullable HttpMessageContentConverter contentConverter; /** @@ -78,8 +77,7 @@ public String getJson() { /** * Return the {@link HttpMessageContentConverter} to use to deserialize content. */ - @Nullable - HttpMessageContentConverter getContentConverter() { + @Nullable HttpMessageContentConverter getContentConverter() { return this.contentConverter; } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java index 9728a762496a..b5bacb0ebf57 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonContentAssert.java @@ -16,7 +16,7 @@ package org.springframework.test.json; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * Default {@link AbstractJsonContentAssert} implementation. diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java index fe905c000a17..dbf5f44580bb 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonLoader.java @@ -22,9 +22,10 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; -import org.springframework.lang.Nullable; import org.springframework.util.FileCopyUtils; /** @@ -37,8 +38,7 @@ */ class JsonLoader { - @Nullable - private final Class resourceLoadClass; + private final @Nullable Class resourceLoadClass; private final Charset charset; @@ -49,8 +49,7 @@ class JsonLoader { } - @Nullable - String getJson(@Nullable CharSequence source) { + @Nullable String getJson(@Nullable CharSequence source) { if (source == null) { return null; } diff --git a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java index c30843e87c57..1dd07ba0e33e 100644 --- a/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java +++ b/spring-test/src/main/java/org/springframework/test/json/JsonPathValueAssert.java @@ -17,8 +17,8 @@ package org.springframework.test.json; import com.jayway.jsonpath.JsonPath; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.http.HttpMessageContentConverter; /** diff --git a/spring-test/src/main/java/org/springframework/test/json/package-info.java b/spring-test/src/main/java/org/springframework/test/json/package-info.java index cf1085f3b403..75c7dff9f87d 100644 --- a/spring-test/src/main/java/org/springframework/test/json/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/json/package-info.java @@ -1,9 +1,7 @@ /** * Testing support for JSON. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.json; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java index db51fb160c0a..80815647e5d8 100644 --- a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java +++ b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java @@ -16,8 +16,9 @@ package org.springframework.test.util; +import org.jspecify.annotations.Nullable; + import org.springframework.lang.Contract; -import org.springframework.lang.Nullable; import org.springframework.util.ObjectUtils; /** diff --git a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java index 00771118bed5..fdd432c7671c 100644 --- a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.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,9 +29,9 @@ import org.hamcrest.CoreMatchers; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; @@ -342,8 +342,7 @@ private String failureReason(String expectedDescription, @Nullable Object value) * @return the result of the evaluation * @throws AssertionError if the evaluation fails */ - @Nullable - public Object evaluateJsonPath(String content) { + public @Nullable Object evaluateJsonPath(String content) { try { return this.jsonPath.read(content, this.configuration); } @@ -383,8 +382,7 @@ public T evaluateJsonPath(String content, ParameterizedTypeReference targ context.read(this.expression, new TypeRefAdapter<>(targetType))); } - @Nullable - private Object assertExistsAndReturn(String content) { + private @Nullable Object assertExistsAndReturn(String content) { Object value = evaluateJsonPath(content); String reason = "No value at JSON path \"" + this.expression + "\""; AssertionErrors.assertTrue(reason, value != null); diff --git a/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java b/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java index a346aa6a548f..81a57afddc9a 100644 --- a/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java +++ b/spring-test/src/main/java/org/springframework/test/util/MethodAssert.java @@ -20,8 +20,7 @@ import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; - -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * AssertJ {@link org.assertj.core.api.Assert assertions} that can be applied diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java index f63738405e9f..f87ca385b6e5 100644 --- a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java @@ -21,9 +21,9 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import org.springframework.aop.support.AopUtils; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.MethodInvoker; @@ -173,7 +173,7 @@ public static void setField( * @see ReflectionUtils#setField(Field, Object, Object) * @see AopTestUtils#getUltimateTargetObject(Object) */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public static void setField(@Nullable Object targetObject, @Nullable Class targetClass, @Nullable String name, @Nullable Object value, @Nullable Class type) { @@ -214,8 +214,7 @@ public static void setField(@Nullable Object targetObject, @Nullable Class ta * @return the field's current value * @see #getField(Class, String) */ - @Nullable - public static Object getField(Object targetObject, String name) { + public static @Nullable Object getField(Object targetObject, String name) { return getField(targetObject, null, name); } @@ -231,8 +230,7 @@ public static Object getField(Object targetObject, String name) { * @since 4.2 * @see #getField(Object, String) */ - @Nullable - public static Object getField(Class targetClass, String name) { + public static @Nullable Object getField(Class targetClass, String name) { return getField(null, targetClass, name); } @@ -260,9 +258,8 @@ public static Object getField(Class targetClass, String name) { * @see ReflectionUtils#getField(Field, Object) * @see AopTestUtils#getUltimateTargetObject(Object) */ - @Nullable - @SuppressWarnings("NullAway") - public static Object getField(@Nullable Object targetObject, @Nullable Class targetClass, String name) { + @SuppressWarnings("NullAway") // Dataflow analysis limitation + public static @Nullable Object getField(@Nullable Object targetObject, @Nullable Class targetClass, String name) { Assert.isTrue(targetObject != null || targetClass != null, "Either targetObject or targetClass for the field must be specified"); @@ -391,8 +388,7 @@ public static void invokeSetterMethod(Object target, String name, @Nullable Obje * @see ReflectionUtils#invokeMethod(Method, Object, Object[]) * @see AopTestUtils#getUltimateTargetObject(Object) */ - @Nullable - public static Object invokeGetterMethod(Object target, String name) { + public static @Nullable Object invokeGetterMethod(Object target, String name) { Assert.notNull(target, "Target object must not be null"); Assert.hasText(name, "Method name must not be empty"); @@ -437,8 +433,7 @@ public static Object invokeGetterMethod(Object target, String name) { * @see #invokeMethod(Class, String, Object...) * @see #invokeMethod(Object, Class, String, Object...) */ - @Nullable - public static T invokeMethod(Object target, String name, Object... args) { + public static @Nullable T invokeMethod(Object target, String name, Object... args) { Assert.notNull(target, "Target object must not be null"); return invokeMethod(target, null, name, args); } @@ -456,8 +451,7 @@ public static T invokeMethod(Object target, String name, Object... args) { * @see #invokeMethod(Object, String, Object...) * @see #invokeMethod(Object, Class, String, Object...) */ - @Nullable - public static T invokeMethod(Class targetClass, String name, Object... args) { + public static @Nullable T invokeMethod(Class targetClass, String name, Object... args) { Assert.notNull(targetClass, "Target class must not be null"); return invokeMethod(null, targetClass, name, args); } @@ -489,8 +483,7 @@ public static T invokeMethod(Class targetClass, String name, Object... ar * @see AopTestUtils#getUltimateTargetObject(Object) */ @SuppressWarnings("unchecked") - @Nullable - public static T invokeMethod(@Nullable Object targetObject, @Nullable Class targetClass, String name, + public static @Nullable T invokeMethod(@Nullable Object targetObject, @Nullable Class targetClass, String name, Object... args) { Assert.isTrue(targetObject != null || targetClass != null, 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 3f1b8ae3ac74..201c9b70d7c6 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 @@ * 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/util/XpathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java index 046c0d406f22..18aabd19a994 100644 --- a/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/XpathExpectationsHelper.java @@ -31,12 +31,12 @@ import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Document; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.InputSource; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import org.springframework.util.xml.SimpleNamespaceContext; @@ -214,8 +214,7 @@ public void assertBoolean(byte[] content, @Nullable String encoding, boolean exp * @throws Exception if content parsing or expression evaluation fails * @since 5.1 */ - @Nullable - public T evaluateXpath(byte[] content, @Nullable String encoding, Class targetClass) throws Exception { + public @Nullable T evaluateXpath(byte[] content, @Nullable String encoding, Class targetClass) throws Exception { Document document = parseXmlByteArray(content, encoding); return evaluateXpath(document, toQName(targetClass), targetClass); } @@ -242,8 +241,7 @@ protected Document parseXmlByteArray(byte[] xml, @Nullable String encoding) thro * @throws XPathExpressionException if expression evaluation failed */ @SuppressWarnings("unchecked") - @Nullable - protected T evaluateXpath(Document document, QName evaluationType, Class expectedClass) + protected @Nullable T evaluateXpath(Document document, QName evaluationType, Class expectedClass) throws XPathExpressionException { return (T) getXpathExpression().evaluate(document, evaluationType); diff --git a/spring-test/src/main/java/org/springframework/test/util/package-info.java b/spring-test/src/main/java/org/springframework/test/util/package-info.java index aa08fffb7904..12a3726e0675 100644 --- a/spring-test/src/main/java/org/springframework/test/util/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/util/package-info.java @@ -1,9 +1,7 @@ /** * General utility classes for use in unit and integration tests. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.util; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/validation/package-info.java b/spring-test/src/main/java/org/springframework/test/validation/package-info.java index caa3fdcadda3..d4348ea12e8a 100644 --- a/spring-test/src/main/java/org/springframework/test/validation/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/validation/package-info.java @@ -1,9 +1,7 @@ /** * Testing support for validation. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.validation; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java index 0d87fdfe257c..5e553b6f0265 100644 --- a/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/ModelAndViewAssert.java @@ -109,7 +109,7 @@ public static void assertModelAttributeValue(ModelAndView mav, String modelName, * @param mav the ModelAndView to test against (never {@code null}) * @param expectedModel the expected model */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public static void assertModelAttributeValues(ModelAndView mav, Map expectedModel) { Map model = mav.getModel(); diff --git a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java index 7fac1976fbb3..f532481f162b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/UriAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/UriAssert.java @@ -20,8 +20,8 @@ import org.assertj.core.api.Assertions; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.util.AntPathMatcher; import org.springframework.web.util.UriComponentsBuilder; @@ -56,7 +56,7 @@ public UriAssert(@Nullable String actual, String displayName) { * @param uriVars the values to replace the URI template variables * @see UriComponentsBuilder#buildAndExpand(Object...) */ - public UriAssert isEqualToTemplate(String uriTemplate, Object... uriVars) { + public UriAssert isEqualToTemplate(String uriTemplate, @Nullable Object... uriVars) { String uri = buildUri(uriTemplate, uriVars); return isEqualTo(uri); } @@ -80,22 +80,32 @@ public UriAssert matchesAntPattern(String uriPattern) { return this; } - @SuppressWarnings("NullAway") - private String buildUri(String uriTemplate, Object... uriVars) { + + private String buildUri(String uriTemplate, @Nullable Object... uriVars) { try { return UriComponentsBuilder.fromUriString(uriTemplate) .buildAndExpand(uriVars).encode().toUriString(); } catch (Exception ex) { + String message = ex.getMessage(); throw Failures.instance().failure(this.info, - new ShouldBeValidUriTemplate(uriTemplate, ex.getMessage())); + message == null ? + new ShouldBeValidUriTemplate(uriTemplate) : + new ShouldBeValidUriTemplateWithMessage(uriTemplate, message)); } } private static final class ShouldBeValidUriTemplate extends BasicErrorMessageFactory { - private ShouldBeValidUriTemplate(String uriTemplate, String errorMessage) { + private ShouldBeValidUriTemplate(String uriTemplate) { + super("%nExpecting:%n %s%nTo be a valid URI template%n", uriTemplate); + } + } + + private static final class ShouldBeValidUriTemplateWithMessage extends BasicErrorMessageFactory { + + private ShouldBeValidUriTemplateWithMessage(String uriTemplate, String errorMessage) { super("%nExpecting:%n %s%nTo be a valid URI template but got:%n %s%n", uriTemplate, errorMessage); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java index 6e075ff93074..07c91a4afe6a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -30,10 +30,11 @@ import java.util.Set; import java.util.stream.Collectors; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -219,8 +220,7 @@ public Set getExpectations() { /** * Return a matching expectation, or {@code null} if none match. */ - @Nullable - public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException { + public @Nullable RequestExpectation findExpectation(ClientHttpRequest request) throws IOException { for (RequestExpectation expectation : this.expectations) { try { expectation.match(request); diff --git a/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java index d1fc550fa174..7ebbbad16caf 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java @@ -20,9 +20,10 @@ import java.util.ArrayList; import java.util.List; +import org.jspecify.annotations.Nullable; + import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -38,8 +39,7 @@ public class DefaultRequestExpectation implements RequestExpectation { private final List requestMatchers = new ArrayList<>(1); - @Nullable - private ResponseCreator responseCreator; + private @Nullable ResponseCreator responseCreator; /** @@ -63,8 +63,7 @@ protected List getRequestMatchers() { return this.requestMatchers; } - @Nullable - protected ResponseCreator getResponseCreator() { + protected @Nullable ResponseCreator getResponseCreator() { return this.responseCreator; } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java index eb6852961916..03fba3533a21 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.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. @@ -244,6 +244,10 @@ public MockRestServiceServerBuilder bufferContent() { return this; } + protected boolean shouldBufferContent() { + return this.bufferContent; + } + @Override public MockRestServiceServer build() { if (this.ignoreExpectOrder) { @@ -258,9 +262,6 @@ public MockRestServiceServer build() { public MockRestServiceServer build(RequestExpectationManager manager) { MockRestServiceServer server = new MockRestServiceServer(manager); ClientHttpRequestFactory factory = server.new MockClientHttpRequestFactory(); - if (this.bufferContent) { - factory = new BufferingClientHttpRequestFactory(factory); - } injectRequestFactory(factory); return server; } @@ -281,6 +282,9 @@ private static class RestClientMockRestServiceServerBuilder extends AbstractMock @Override protected void injectRequestFactory(ClientHttpRequestFactory requestFactory) { + if (shouldBufferContent()) { + this.restClientBuilder.bufferContent((uri, httpMethod) -> true); + } this.restClientBuilder.requestFactory(requestFactory); } } @@ -297,6 +301,9 @@ private static class RestTemplateMockRestServiceServerBuilder extends AbstractMo @Override protected void injectRequestFactory(ClientHttpRequestFactory requestFactory) { + if (shouldBufferContent()) { + this.restTemplate.setBufferingPredicate((uri, httpMethod) -> true); + } this.restTemplate.setRequestFactory(requestFactory); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/ResponseCreator.java b/spring-test/src/main/java/org/springframework/test/web/client/ResponseCreator.java index c9eb4e9c6842..990d9b2d8910 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/ResponseCreator.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/ResponseCreator.java @@ -18,9 +18,10 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.test.web.client.response.MockRestResponseCreators; /** diff --git a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java index 12799b25880d..b8c66ef83208 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java @@ -19,8 +19,9 @@ import java.io.IOException; import java.util.Iterator; +import org.jspecify.annotations.Nullable; + import org.springframework.http.client.ClientHttpRequest; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -38,8 +39,7 @@ public class SimpleRequestExpectationManager extends AbstractRequestExpectationManager { /** Expectations in the order of declaration (count may be > 1). */ - @Nullable - private Iterator expectationIterator; + private @Nullable Iterator expectationIterator; /** Track expectations that have a remaining count. */ private final RequestExpectationGroup repeatExpectations = new RequestExpectationGroup(); diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index de937e6051bd..fb9cb6020451 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -32,6 +32,7 @@ import org.apache.tomcat.util.http.fileupload.UploadContext; import org.apache.tomcat.util.http.fileupload.disk.DiskFileItemFactory; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Node; import org.springframework.core.io.Resource; @@ -39,7 +40,6 @@ import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.converter.FormHttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.mock.http.MockHttpInputMessage; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.test.json.JsonAssert; @@ -424,13 +424,11 @@ private static class MultipartHelper { List fileItems = fileUpload.parseRequest(new UploadContext() { private final byte[] body = request.getBodyAsBytes(); @Override - @Nullable - public String getCharacterEncoding() { + public @Nullable String getCharacterEncoding() { return request.getHeaders().getFirst(HttpHeaders.CONTENT_ENCODING); } @Override - @Nullable - public String getContentType() { + public @Nullable String getContentType() { return request.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); } @Override 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 203b4a853827..f28300c382b7 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 @@ -1,5 +1,5 @@ /* - * Copyright 2002-2023 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,11 +19,14 @@ import java.net.URI; import java.util.List; import java.util.Map; +import java.util.Set; import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; +import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.client.ClientHttpRequest; import org.springframework.test.web.client.MockRestServiceServer; @@ -97,7 +100,7 @@ public static RequestMatcher requestTo(String expectedUri) { * @param uriVars zero or more URI variables to populate the expected URI * @return the request matcher */ - public static RequestMatcher requestToUriTemplate(String expectedUri, Object... uriVars) { + public static RequestMatcher requestToUriTemplate(String expectedUri, @Nullable Object... uriVars) { Assert.notNull(expectedUri, "'uri' must not be null"); URI uri = UriComponentsBuilder.fromUriString(expectedUri).buildAndExpand(uriVars).encode().toUri(); return requestTo(uri); @@ -128,6 +131,7 @@ public static RequestMatcher requestTo(URI uri) { * @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 -> { @@ -156,13 +160,14 @@ public static RequestMatcher queryParamList(String name, Matcher... matchers) { return request -> { MultiValueMap params = getQueryParams(request); - assertValueCount("query param", name, params, matchers.length); + assertValueCount(name, params, matchers.length); for (int i = 0 ; i < matchers.length; i++) { assertThat("Query param", params.get(name).get(i), matchers[i]); } @@ -185,18 +190,38 @@ public static RequestMatcher queryParam(String name, Matcher... * parameter value * @see #queryParamList(String, Matcher) * @see #queryParam(String, Matcher...) + * @see #queryParamCount(int) */ - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public static RequestMatcher queryParam(String name, String... expectedValues) { return request -> { MultiValueMap params = getQueryParams(request); - assertValueCount("query param", name, params, expectedValues.length); + assertValueCount(name, params, expectedValues.length); for (int i = 0 ; i < expectedValues.length; i++) { assertEquals("Query param [" + name + "]", expectedValues[i], params.get(name).get(i)); } }; } + /** + * 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(); } @@ -246,7 +271,7 @@ public static RequestMatcher headerList(String name, Matcher... matchers) { return request -> { - assertValueCount("header", name, request.getHeaders(), matchers.length); + assertValueCount(name, request.getHeaders(), matchers.length); List headerValues = request.getHeaders().get(name); Assert.state(headerValues != null, "No header values"); for (int i = 0; i < matchers.length; i++) { @@ -273,7 +298,7 @@ public static RequestMatcher header(String name, Matcher... matc */ public static RequestMatcher header(String name, String... expectedValues) { return request -> { - assertValueCount("header", name, request.getHeaders(), expectedValues.length); + assertValueCount(name, request.getHeaders(), expectedValues.length); List headerValues = request.getHeaders().get(name); Assert.state(headerValues != null, "No header values"); for (int i = 0; i < expectedValues.length; i++) { @@ -356,11 +381,20 @@ public static XpathRequestMatchers xpath(String expression, Map } - private static void assertValueCount( - String valueType, String name, MultiValueMap map, int count) { - + private static void assertValueCount(String name, MultiValueMap map, int count) { List values = map.get(name); - String message = "Expected " + valueType + " <" + name + ">"; + String message = "Expected query param <" + name + ">"; + if (values == null) { + fail(message + " to exist but was null"); + } + else if (count > values.size()) { + fail(message + " to have at least <" + count + "> values but found " + values); + } + } + + private static void assertValueCount(String name, HttpHeaders headers, int count) { + List values = headers.get(name); + String message = "Expected header <" + name + ">"; if (values == null) { fail(message + " to exist but was null"); } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/XpathRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/XpathRequestMatchers.java index da90493f25b7..a539a1b35f1d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/XpathRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/XpathRequestMatchers.java @@ -21,10 +21,10 @@ import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Node; import org.springframework.http.client.ClientHttpRequest; -import org.springframework.lang.Nullable; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.test.util.XpathExpectationsHelper; import org.springframework.test.web.client.RequestMatcher; diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/package-info.java b/spring-test/src/main/java/org/springframework/test/web/client/match/package-info.java index cb8a23d931aa..e8ce5ff39570 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/package-info.java @@ -4,9 +4,7 @@ * {@link org.springframework.test.web.client.match.MockRestRequestMatchers} * to gain access to instances of those implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.client.match; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/client/package-info.java b/spring-test/src/main/java/org/springframework/test/web/client/package-info.java index ecfaf0c4a6c1..ddcf087c76b0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/package-info.java @@ -2,9 +2,7 @@ * Contains client-side REST testing support. * @see org.springframework.test.web.client.MockRestServiceServer */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.client; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java b/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java index 745f59e4b22b..bed82cc6e9c0 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java @@ -21,6 +21,8 @@ import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatusCode; @@ -28,7 +30,6 @@ import org.springframework.http.ResponseCookie; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.mock.http.client.MockClientHttpResponse; import org.springframework.test.web.client.ResponseCreator; import org.springframework.util.Assert; @@ -46,8 +47,7 @@ public class DefaultResponseCreator implements ResponseCreator { private byte[] content = new byte[0]; - @Nullable - private Resource contentResource; + private @Nullable Resource contentResource; private final HttpHeaders headers = new HttpHeaders(); diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/ExecutingResponseCreator.java b/spring-test/src/main/java/org/springframework/test/web/client/response/ExecutingResponseCreator.java index cbaa820b92f3..9ab07a724b57 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/ExecutingResponseCreator.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/ExecutingResponseCreator.java @@ -18,10 +18,11 @@ import java.io.IOException; +import org.jspecify.annotations.Nullable; + import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; import org.springframework.http.client.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.test.web.client.ResponseCreator; import org.springframework.util.Assert; diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java b/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java index dd84f7362496..1cf47771f3a9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/MockRestResponseCreators.java @@ -19,12 +19,13 @@ import java.io.IOException; import java.net.URI; +import org.jspecify.annotations.Nullable; + import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatusCode; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.test.web.client.ResponseCreator; /** diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/package-info.java b/spring-test/src/main/java/org/springframework/test/web/client/response/package-info.java index 242530cd1436..aaa3062c043b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/package-info.java @@ -4,9 +4,7 @@ * {@link org.springframework.test.web.client.response.MockRestResponseCreators} * to gain access to instances of those implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.client.response; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/package-info.java b/spring-test/src/main/java/org/springframework/test/web/package-info.java index 932a82dcfa3d..79de8a4d9270 100644 --- a/spring-test/src/main/java/org/springframework/test/web/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/package-info.java @@ -1,9 +1,7 @@ /** * Helper classes for unit tests based on Spring's web support. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java index 856fe3f71560..21dbcf3901b3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/AbstractMockServerSpec.java @@ -20,7 +20,8 @@ import java.util.Arrays; import java.util.List; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.util.CollectionUtils; import org.springframework.web.server.WebFilter; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; @@ -37,14 +38,11 @@ abstract class AbstractMockServerSpec> implements WebTestClient.MockServerSpec { - @Nullable - private List filters; + private @Nullable List filters; - @Nullable - private WebSessionManager sessionManager; + private @Nullable WebSessionManager sessionManager; - @Nullable - private List configurers; + private @Nullable List configurers; AbstractMockServerSpec() { 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 900a985c25a4..32b938cb5d92 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. @@ -20,16 +20,18 @@ import java.util.List; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.BeanUtils; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.format.FormatterRegistry; import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.lang.Nullable; 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; @@ -117,6 +119,12 @@ public DefaultControllerSpec validator(Validator validator) { return this; } + @Override + public WebTestClient.ControllerSpec apiVersioning(Consumer configurer) { + this.configurer.versionConsumer = configurer; + return this; + } + @Override public DefaultControllerSpec viewResolvers(Consumer consumer) { this.configurer.viewResolversConsumer = consumer; @@ -153,32 +161,25 @@ private ApplicationContext initApplicationContext() { private static class TestWebFluxConfigurer implements WebFluxConfigurer { - @Nullable - private Consumer contentTypeResolverConsumer; + private @Nullable Consumer contentTypeResolverConsumer; - @Nullable - private Consumer corsRegistryConsumer; + private @Nullable Consumer corsRegistryConsumer; - @Nullable - private Consumer argumentResolverConsumer; + private @Nullable Consumer argumentResolverConsumer; - @Nullable - private Consumer pathMatchConsumer; + private @Nullable Consumer pathMatchConsumer; - @Nullable - private Consumer messageCodecsConsumer; + private @Nullable Consumer messageCodecsConsumer; - @Nullable - private Consumer formattersConsumer; + private @Nullable Consumer formattersConsumer; - @Nullable - private Validator validator; + private @Nullable Validator validator; - @Nullable - private Consumer viewResolversConsumer; + private @Nullable Consumer versionConsumer; - @Nullable - private Consumer executionConsumer; + private @Nullable Consumer viewResolversConsumer; + + private @Nullable Consumer executionConsumer; @Override public void configureContentTypeResolver(RequestedContentTypeResolverBuilder builder) { @@ -223,11 +224,17 @@ public void addFormatters(FormatterRegistry registry) { } @Override - @Nullable - public Validator getValidator() { + public @Nullable Validator getValidator() { return this.validator; } + @Override + public void configureApiVersioning(ApiVersionConfigurer configurer) { + if (this.versionConsumer != null) { + this.versionConsumer.accept(configurer); + } + } + @Override public void configureViewResolvers(ViewResolverRegistry registry) { if (this.viewResolversConsumer != null) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java index 5b8597a68ec5..60b39d57e76b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/DefaultWebTestClient.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 com.jayway.jsonpath.spi.mapper.MappingProvider; import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; @@ -44,7 +45,6 @@ import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.ClientHttpRequest; -import org.springframework.lang.Nullable; import org.springframework.test.json.JsonAssert; import org.springframework.test.json.JsonComparator; import org.springframework.test.json.JsonCompareMode; @@ -56,6 +56,7 @@ import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MimeType; import org.springframework.util.MultiValueMap; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.reactive.function.BodyInserter; import org.springframework.web.reactive.function.BodyInserters; import org.springframework.web.reactive.function.client.ClientRequest; @@ -78,18 +79,19 @@ class DefaultWebTestClient implements WebTestClient { private final WiretapConnector wiretapConnector; - @Nullable - private final JsonEncoderDecoder jsonEncoderDecoder; + private final @Nullable JsonEncoderDecoder jsonEncoderDecoder; private final ExchangeFunction exchangeFunction; private final UriBuilderFactory uriBuilderFactory; - @Nullable - private final HttpHeaders defaultHeaders; + private final @Nullable HttpHeaders defaultHeaders; - @Nullable - private final MultiValueMap defaultCookies; + private final @Nullable MultiValueMap defaultCookies; + + private final @Nullable Object defaultApiVersion; + + private final @Nullable ApiVersionInserter apiVersionInserter; private final Consumer> entityResultConsumer; @@ -100,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) { @@ -113,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; @@ -183,21 +189,19 @@ private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec { private final HttpMethod httpMethod; - @Nullable - private URI uri; + private @Nullable URI uri; private final HttpHeaders headers; - @Nullable - private MultiValueMap cookies; + private @Nullable MultiValueMap cookies; + + private @Nullable Object apiVersion; - @Nullable - private BodyInserter inserter; + private @Nullable BodyInserter inserter; private final Map attributes = new LinkedHashMap<>(4); - @Nullable - private String uriTemplate; + private @Nullable String uriTemplate; private final String requestId; @@ -209,13 +213,13 @@ private class DefaultRequestBodyUriSpec implements RequestBodyUriSpec { } @Override - public RequestBodySpec uri(String uriTemplate, Object... uriVariables) { + public RequestBodySpec uri(String uriTemplate, @Nullable Object... uriVariables) { this.uriTemplate = uriTemplate; return uri(DefaultWebTestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables)); } @Override - public RequestBodySpec uri(String uriTemplate, Map uriVariables) { + public RequestBodySpec uri(String uriTemplate, Map uriVariables) { this.uriTemplate = uriTemplate; return uri(DefaultWebTestClient.this.uriBuilderFactory.expand(uriTemplate, uriVariables)); } @@ -317,6 +321,12 @@ public RequestBodySpec ifNoneMatch(String... ifNoneMatches) { return this; } + @Override + public RequestBodySpec apiVersion(Object version) { + this.apiVersion = version; + return this; + } + @Override public RequestHeadersSpec bodyValue(Object body) { this.inserter = BodyInserters.fromValue(body); @@ -354,12 +364,6 @@ public RequestHeadersSpec body(BodyInserter ins return this; } - @Override - @Deprecated - public RequestHeadersSpec syncBody(Object body) { - return bodyValue(body); - } - @Override public ResponseSpec exchange() { ClientRequest request = (this.inserter != null ? @@ -380,12 +384,17 @@ public ResponseSpec exchange() { private ClientRequest.Builder initRequestBuilder() { return ClientRequest.create(this.httpMethod, initUri()) .headers(headersToUse -> { - if (!CollectionUtils.isEmpty(DefaultWebTestClient.this.defaultHeaders)) { + if (!(DefaultWebTestClient.this.defaultHeaders == null || DefaultWebTestClient.this.defaultHeaders.isEmpty())) { headersToUse.putAll(DefaultWebTestClient.this.defaultHeaders); } - if (!CollectionUtils.isEmpty(this.headers)) { + 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)) { @@ -399,7 +408,17 @@ private ClientRequest.Builder initRequestBuilder() { } 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); } } @@ -411,8 +430,7 @@ private static class DefaultResponseSpec implements ResponseSpec { private final ClientResponse response; - @Nullable - private final JsonEncoderDecoder jsonEncoderDecoder; + private final @Nullable JsonEncoderDecoder jsonEncoderDecoder; private final Consumer> entityResultConsumer; @@ -555,20 +573,21 @@ protected EntityExchangeResult getResult() { } @Override - public T isEqualTo(B expected) { + public T isEqualTo(@Nullable B expected) { this.result.assertWithDiagnostics(() -> AssertionErrors.assertEquals("Response body", expected, this.result.getResponseBody())); return self(); } @Override - public T value(Matcher matcher) { + public T value(Matcher matcher) { this.result.assertWithDiagnostics(() -> MatcherAssert.assertThat(this.result.getResponseBody(), matcher)); return self(); } @Override - public T value(Function bodyMapper, Matcher matcher) { + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1129 + public T value(Function<@Nullable B, @Nullable R> bodyMapper, Matcher matcher) { this.result.assertWithDiagnostics(() -> { B body = this.result.getResponseBody(); MatcherAssert.assertThat(bodyMapper.apply(body), matcher); @@ -577,7 +596,8 @@ public T value(Function bodyMapper, Matcher ma } @Override - public T value(Consumer consumer) { + @SuppressWarnings("NullAway") // https://github.com/uber/NullAway/issues/1129 + public T value(Consumer<@Nullable B> consumer) { this.result.assertWithDiagnostics(() -> consumer.accept(this.result.getResponseBody())); return self(); } @@ -600,7 +620,7 @@ public EntityExchangeResult returnResult() { } - private static class DefaultListBodySpec extends DefaultBodySpec, ListBodySpec> + private static class DefaultListBodySpec extends DefaultBodySpec, ListBodySpec> implements ListBodySpec { DefaultListBodySpec(EntityExchangeResult> result) { @@ -609,7 +629,7 @@ private static class DefaultListBodySpec extends DefaultBodySpec, Lis @Override public ListBodySpec hasSize(int size) { - List actual = getResult().getResponseBody(); + List<@Nullable E> actual = getResult().getResponseBody(); String message = "Response body does not contain " + size + " elements"; getResult().assertWithDiagnostics(() -> AssertionErrors.assertEquals(message, size, (actual != null ? actual.size() : 0))); @@ -618,9 +638,9 @@ public ListBodySpec hasSize(int size) { @Override @SuppressWarnings("unchecked") - public ListBodySpec contains(E... elements) { + public ListBodySpec contains(@Nullable E... elements) { List expected = Arrays.asList(elements); - List actual = getResult().getResponseBody(); + List<@Nullable E> actual = getResult().getResponseBody(); String message = "Response body does not contain " + expected; getResult().assertWithDiagnostics(() -> AssertionErrors.assertTrue(message, (actual != null && actual.containsAll(expected)))); @@ -629,9 +649,9 @@ public ListBodySpec contains(E... elements) { @Override @SuppressWarnings("unchecked") - public ListBodySpec doesNotContain(E... elements) { + public ListBodySpec doesNotContain(@Nullable E... elements) { List expected = Arrays.asList(elements); - List actual = getResult().getResponseBody(); + List<@Nullable E> actual = getResult().getResponseBody(); String message = "Response body should not have contained " + expected; getResult().assertWithDiagnostics(() -> AssertionErrors.assertTrue(message, (actual == null || !actual.containsAll(expected)))); @@ -639,7 +659,7 @@ public ListBodySpec doesNotContain(E... elements) { } @Override - public EntityExchangeResult> returnResult() { + public EntityExchangeResult> returnResult() { return getResult(); } } @@ -649,8 +669,7 @@ private static class DefaultBodyContentSpec implements BodyContentSpec { private final EntityExchangeResult result; - @Nullable - private final JsonEncoderDecoder jsonEncoderDecoder; + private final @Nullable JsonEncoderDecoder jsonEncoderDecoder; private final boolean isEmpty; @@ -711,13 +730,6 @@ public JsonPathAssertions jsonPath(String expression) { JsonPathConfigurationProvider.getConfiguration(this.jsonEncoderDecoder)); } - @Override - @SuppressWarnings("removal") - public JsonPathAssertions jsonPath(String expression, Object... args) { - Assert.hasText(expression, "expression must not be null or empty"); - return jsonPath(expression.formatted(args)); - } - @Override public XpathAssertions xpath(String expression, @Nullable Map namespaces, Object... args) { return new XpathAssertions(this, expression, namespaces, args); 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 61a5e47f5a62..70dd95a06233 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 @@ -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,20 +23,21 @@ import java.util.function.Consumer; import java.util.function.Function; +import org.jspecify.annotations.Nullable; + import org.springframework.http.HttpHeaders; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.client.reactive.HttpComponentsClientHttpConnector; import org.springframework.http.client.reactive.JdkClientHttpConnector; import org.springframework.http.client.reactive.JettyClientHttpConnector; import org.springframework.http.client.reactive.ReactorClientHttpConnector; -import org.springframework.http.client.reactive.ReactorNetty2ClientHttpConnector; import org.springframework.http.codec.ClientCodecConfigurer; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; 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; @@ -55,8 +56,6 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { private static final boolean reactorNettyClientPresent; - private static final boolean reactorNetty2ClientPresent; - private static final boolean jettyClientPresent; private static final boolean httpComponentsClientPresent; @@ -66,7 +65,6 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { static { ClassLoader loader = DefaultWebTestClientBuilder.class.getClassLoader(); reactorNettyClientPresent = ClassUtils.isPresent("reactor.netty.http.client.HttpClient", loader); - reactorNetty2ClientPresent = ClassUtils.isPresent("reactor.netty5.http.client.HttpClient", loader); jettyClientPresent = ClassUtils.isPresent("org.eclipse.jetty.client.HttpClient", loader); httpComponentsClientPresent = ClassUtils.isPresent("org.apache.hc.client5.http.impl.async.CloseableHttpAsyncClient", loader) && @@ -76,37 +74,31 @@ class DefaultWebTestClientBuilder implements WebTestClient.Builder { } - @Nullable - private final WebHttpHandlerBuilder httpHandlerBuilder; + private final @Nullable WebHttpHandlerBuilder httpHandlerBuilder; + + private @Nullable ClientHttpConnector connector; - @Nullable - private ClientHttpConnector connector; + private @Nullable String baseUrl; - @Nullable - private String baseUrl; + private @Nullable UriBuilderFactory uriBuilderFactory; - @Nullable - private UriBuilderFactory uriBuilderFactory; + private @Nullable HttpHeaders defaultHeaders; - @Nullable - private HttpHeaders defaultHeaders; + private @Nullable MultiValueMap defaultCookies; - @Nullable - private MultiValueMap defaultCookies; + private @Nullable Object defaultApiVersion; - @Nullable - private List filters; + private @Nullable ApiVersionInserter apiVersionInserter; + + private @Nullable List filters; private Consumer> entityResultConsumer = result -> {}; - @Nullable - private ExchangeStrategies strategies; + private @Nullable ExchangeStrategies strategies; - @Nullable - private List> strategiesConfigurers; + private @Nullable List> strategiesConfigurers; - @Nullable - private Duration responseTimeout; + private @Nullable Duration responseTimeout; /** Determine connector via classpath detection. */ @@ -155,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; @@ -213,6 +207,18 @@ private MultiValueMap initCookies() { 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"); @@ -255,16 +261,6 @@ public WebTestClient.Builder exchangeStrategies(ExchangeStrategies strategies) { return this; } - @Override - @Deprecated - public WebTestClient.Builder exchangeStrategies(Consumer configurer) { - if (this.strategiesConfigurers == null) { - this.strategiesConfigurers = new ArrayList<>(4); - } - this.strategiesConfigurers.add(configurer); - return this; - } - @Override public WebTestClient.Builder apply(WebTestClientConfigurer configurer) { configurer.afterConfigurerAdded(this, this.httpHandlerBuilder, this.connector); @@ -306,19 +302,18 @@ public WebTestClient build() { .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() { if (reactorNettyClientPresent) { return new ReactorClientHttpConnector(); } - else if (reactorNetty2ClientPresent) { - return new ReactorNetty2ClientHttpConnector(); - } else if (jettyClientPresent) { return new JettyClientHttpConnector(); } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java index f6a6dc1cddb3..fc5778eb7716 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EncoderDecoderMappingProvider.java @@ -22,6 +22,7 @@ import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.TypeRef; import com.jayway.jsonpath.spi.mapper.MappingProvider; +import org.jspecify.annotations.Nullable; import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; @@ -29,7 +30,6 @@ import org.springframework.core.io.buffer.DataBuffer; import org.springframework.core.io.buffer.DataBufferFactory; import org.springframework.core.io.buffer.DefaultDataBufferFactory; -import org.springframework.lang.Nullable; import org.springframework.util.MimeType; import org.springframework.util.MimeTypeUtils; @@ -56,21 +56,18 @@ public EncoderDecoderMappingProvider(Encoder encoder, Decoder decoder) { } - @Nullable @Override - public T map(Object source, Class targetType, Configuration configuration) { + public @Nullable T map(Object source, Class targetType, Configuration configuration) { return mapToTargetType(source, ResolvableType.forClass(targetType)); } - @Nullable @Override - public T map(Object source, TypeRef targetType, Configuration configuration) { + public @Nullable T map(Object source, TypeRef targetType, Configuration configuration) { return mapToTargetType(source, ResolvableType.forType(targetType.getType())); } @SuppressWarnings("unchecked") - @Nullable - private T mapToTargetType(Object source, ResolvableType targetType) { + private @Nullable T mapToTargetType(Object source, ResolvableType targetType) { DataBufferFactory bufferFactory = DefaultDataBufferFactory.sharedInstance; MimeType mimeType = MimeTypeUtils.APPLICATION_JSON; Map hints = Collections.emptyMap(); diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/EntityExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EntityExchangeResult.java index d071cbf6288a..98c013e08677 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/EntityExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/EntityExchangeResult.java @@ -16,7 +16,7 @@ package org.springframework.test.web.reactive.server; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; /** * {@code ExchangeResult} sub-class that exposes the response body fully @@ -29,8 +29,7 @@ */ public class EntityExchangeResult extends ExchangeResult { - @Nullable - private final T body; + private final @Nullable T body; EntityExchangeResult(ExchangeResult result, @Nullable T body) { @@ -42,8 +41,7 @@ public class EntityExchangeResult extends ExchangeResult { /** * Return the entity extracted from the response body. */ - @Nullable - public T getResponseBody() { + public @Nullable T getResponseBody() { return this.body; } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java index 881b7c0654a4..68a78ddbf0fc 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/ExchangeResult.java @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.http.HttpHeaders; @@ -35,7 +36,6 @@ import org.springframework.http.ResponseCookie; import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.client.reactive.ClientHttpResponse; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.MultiValueMap; @@ -74,11 +74,9 @@ public class ExchangeResult { private final Duration timeout; - @Nullable - private final String uriTemplate; + private final @Nullable String uriTemplate; - @Nullable - private final Object mockServerResult; + private final @Nullable Object mockServerResult; /** Ensure single logging, for example, for expectAll. */ private boolean diagnosticsLogged; @@ -146,8 +144,7 @@ public URI getUrl() { /** * Return the original URI template used to prepare the request, if any. */ - @Nullable - public String getUriTemplate() { + public @Nullable String getUriTemplate() { return this.uriTemplate; } @@ -164,8 +161,7 @@ public HttpHeaders getRequestHeaders() { * for any reason yet, use of this method will trigger consumption. * @throws IllegalStateException if the request body has not been fully written. */ - @Nullable - public byte[] getRequestBodyContent() { + public byte @Nullable [] getRequestBodyContent() { return this.requestBody.block(this.timeout); } @@ -176,16 +172,6 @@ public HttpStatusCode getStatus() { return this.response.getStatusCode(); } - /** - * Return the HTTP status code as an integer. - * @since 5.1.10 - * @deprecated in favor of {@link #getStatus()}, for removal in 7.0 - */ - @Deprecated(since = "6.0", forRemoval = true) - public int getRawStatusCode() { - return getStatus().value(); - } - /** * Return the response headers received from the server. */ @@ -206,8 +192,7 @@ public MultiValueMap getResponseCookies() { * yet, use of this method will trigger consumption. * @throws IllegalStateException if the response has not been fully read. */ - @Nullable - public byte[] getResponseBodyContent() { + public byte @Nullable [] getResponseBodyContent() { return this.responseBody.block(this.timeout); } @@ -217,8 +202,7 @@ public byte[] getResponseBodyContent() { * @since 5.3 * @see org.springframework.test.web.servlet.client.MockMvcWebTestClient#resultActionsFor(ExchangeResult) */ - @Nullable - public Object getMockServerResult() { + public @Nullable Object getMockServerResult() { return this.mockServerResult; } @@ -265,13 +249,12 @@ private String formatStatus(HttpStatusCode statusCode) { } private String formatHeaders(HttpHeaders headers, String delimiter) { - return headers.entrySet().stream() + return headers.headerSet().stream() .map(entry -> entry.getKey() + ": " + entry.getValue()) .collect(Collectors.joining(delimiter)); } - @Nullable - private String formatBody(@Nullable MediaType contentType, Mono body) { + private @Nullable String formatBody(@Nullable MediaType contentType, Mono body) { return body .map(bytes -> { if (contentType == null) { diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java index fe4abf6c857a..4c5548a936cc 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/HeaderAssertions.java @@ -22,12 +22,12 @@ import java.util.function.Consumer; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.springframework.http.CacheControl; import org.springframework.http.ContentDisposition; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.util.CollectionUtils; import static org.hamcrest.MatcherAssert.assertThat; @@ -213,7 +213,7 @@ private List getRequiredValues(String name) { * @since 5.0.3 */ public WebTestClient.ResponseSpec exists(String name) { - if (!getHeaders().containsKey(name)) { + if (!getHeaders().containsHeader(name)) { String message = getMessage(name) + " does not exist"; this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } @@ -224,7 +224,7 @@ public WebTestClient.ResponseSpec exists(String name) { * Expect that the header with the given name is not present. */ public WebTestClient.ResponseSpec doesNotExist(String name) { - if (getHeaders().containsKey(name)) { + if (getHeaders().containsHeader(name)) { String message = getMessage(name) + " exists with value=[" + getHeaders().getFirst(name) + "]"; this.exchangeResult.assertWithDiagnostics(() -> fail(message)); } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java index ae861713ebe4..311e88a804a9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonEncoderDecoder.java @@ -20,6 +20,8 @@ import java.util.Map; import java.util.stream.Stream; +import org.jspecify.annotations.Nullable; + import org.springframework.core.ResolvableType; import org.springframework.core.codec.Decoder; import org.springframework.core.codec.Encoder; @@ -28,7 +30,6 @@ import org.springframework.http.codec.EncoderHttpMessageWriter; import org.springframework.http.codec.HttpMessageReader; import org.springframework.http.codec.HttpMessageWriter; -import org.springframework.lang.Nullable; /** * {@link Encoder} and {@link Decoder} that is able to encode and decode @@ -56,8 +57,7 @@ record JsonEncoderDecoder(Encoder encoder, Decoder decoder) { * @return a {@link JsonEncoderDecoder} or {@code null} if a suitable codec * is not available */ - @Nullable - static JsonEncoderDecoder from(Collection> messageWriters, + static @Nullable JsonEncoderDecoder from(Collection> messageWriters, Collection> messageReaders) { Encoder jsonEncoder = findJsonEncoder(messageWriters); @@ -75,15 +75,13 @@ static JsonEncoderDecoder from(Collection> messageWriters, * @param writers the writers to inspect * @return a suitable JSON {@link Encoder} or {@code null} */ - @Nullable - private static Encoder findJsonEncoder(Collection> writers) { + private static @Nullable Encoder findJsonEncoder(Collection> writers) { return findJsonEncoder(writers.stream() .filter(EncoderHttpMessageWriter.class::isInstance) .map(writer -> ((EncoderHttpMessageWriter) writer).getEncoder())); } - @Nullable - private static Encoder findJsonEncoder(Stream> stream) { + private static @Nullable Encoder findJsonEncoder(Stream> stream) { return stream .filter(encoder -> encoder.canEncode(MAP_TYPE, MediaType.APPLICATION_JSON)) .findFirst() @@ -96,15 +94,13 @@ private static Encoder findJsonEncoder(Stream> stream) { * @param readers the readers to inspect * @return a suitable JSON {@link Decoder} or {@code null} */ - @Nullable - private static Decoder findJsonDecoder(Collection> readers) { + private static @Nullable Decoder findJsonDecoder(Collection> readers) { return findJsonDecoder(readers.stream() .filter(DecoderHttpMessageReader.class::isInstance) .map(reader -> ((DecoderHttpMessageReader) reader).getDecoder())); } - @Nullable - private static Decoder findJsonDecoder(Stream> decoderStream) { + private static @Nullable Decoder findJsonDecoder(Stream> decoderStream) { return decoderStream .filter(decoder -> decoder.canDecode(MAP_TYPE, MediaType.APPLICATION_JSON)) .findFirst() diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java index d761575ac839..2094cc6858b3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/JsonPathAssertions.java @@ -20,9 +20,9 @@ import com.jayway.jsonpath.Configuration; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.lang.Nullable; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.util.Assert; @@ -161,17 +161,6 @@ public WebTestClient.BodyContentSpec value(Class targetType, Matcher WebTestClient.BodyContentSpec value(Matcher matcher, Class targetType) { - this.pathHelper.assertValue(this.content, matcher, targetType); - return this.bodySpec; - } - /** * Delegates to {@link JsonPathExpectationsHelper#assertValue(String, Matcher, ParameterizedTypeReference)}. * @since 6.2 @@ -202,16 +191,6 @@ public WebTestClient.BodyContentSpec value(Class targetType, Consumer return this.bodySpec; } - /** - * Consume the result of the JSONPath evaluation and provide a target class. - * @since 5.1 - * @deprecated in favor of {@link #value(Class, Consumer)} - */ - @Deprecated(since = "6.2", forRemoval = true) - public WebTestClient.BodyContentSpec value(Consumer consumer, Class targetType) { - return value(targetType, consumer); - } - /** * Consume the result of the JSONPath evaluation and provide a parameterized type. * @since 6.2 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 49999e352235..b7018bcdda2f 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 @@ -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 java.util.function.Function; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import org.springframework.context.ApplicationContext; @@ -39,13 +40,15 @@ import org.springframework.http.client.reactive.ClientHttpRequest; import org.springframework.http.codec.ClientCodecConfigurer; import org.springframework.http.codec.ServerCodecConfigurer; -import org.springframework.lang.Nullable; import org.springframework.test.json.JsonComparator; 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 @@ interface ControllerSpec extends MockServerSpec { */ 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 @@ -403,7 +412,7 @@ interface Builder { * Manipulate the default headers with the given consumer. The * headers provided to the consumer are "live", so that the consumer can be used to * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, - * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other * {@link HttpHeaders} methods. * @param headersConsumer a function that consumes the {@code HttpHeaders} * @return this builder @@ -428,6 +437,24 @@ interface Builder { */ 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 @@ -490,16 +517,6 @@ interface Builder { */ Builder exchangeStrategies(ExchangeStrategies strategies); - /** - * Customize the strategies configured via - * {@link #exchangeStrategies(ExchangeStrategies)}. This method is - * designed for use in scenarios where multiple parties wish to update - * the {@code ExchangeStrategies}. - * @deprecated as of 5.1.13 in favor of {@link #codecs(Consumer)} - */ - @Deprecated - Builder exchangeStrategies(Consumer configurer); - /** * Max amount of time to wait for responses. *

      By default 5 seconds. @@ -557,7 +574,7 @@ interface UriSpec> { * with a base URI) it will be used to expand the URI template. * @return spec to add headers or perform the exchange */ - S uri(String uri, Object... uriVariables); + S uri(String uri, @Nullable Object... uriVariables); /** * Specify the URI for the request using a URI template and URI variables. @@ -565,7 +582,7 @@ interface UriSpec> { * with a base URI) it will be used to expand the URI template. * @return spec to add headers or perform the exchange */ - S uri(String uri, Map uriVariables); + S uri(String uri, Map uriVariables); /** * Build the URI for the request with a {@link UriBuilder} obtained @@ -646,13 +663,24 @@ interface RequestHeadersSpec> { * Manipulate the request's headers with the given consumer. The * headers provided to the consumer are "live", so that the consumer can be used to * {@linkplain HttpHeaders#set(String, String) overwrite} existing header values, - * {@linkplain HttpHeaders#remove(Object) remove} values, or use any of the other + * {@linkplain HttpHeaders#remove(String) remove} values, or use any of the other * {@link HttpHeaders} methods. * @param headersConsumer a function that consumes the {@code HttpHeaders} * @return this builder */ 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 @@ -771,15 +799,6 @@ > RequestHeadersSpec body( * @see org.springframework.web.reactive.function.BodyInserters */ RequestHeadersSpec body(BodyInserter inserter); - - /** - * Shortcut for {@link #body(BodyInserter)} with a - * {@linkplain BodyInserters#fromValue value inserter}. - * As of 5.2 this method delegates to {@link #bodyValue(Object)}. - * @deprecated as of Spring Framework 5.2 in favor of {@link #bodyValue(Object)} - */ - @Deprecated - RequestHeadersSpec syncBody(Object body); } @@ -918,26 +937,26 @@ interface BodySpec> { /** * Assert the extracted body is equal to the given value. */ - T isEqualTo(B expected); + T isEqualTo(@Nullable B expected); /** * Assert the extracted body with a {@link Matcher}. * @since 5.1 */ - T value(Matcher matcher); + T value(Matcher matcher); /** * Transform the extracted the body with a function, for example, extracting a * property, and assert the mapped value with a {@link Matcher}. * @since 5.1 */ - T value(Function bodyMapper, Matcher matcher); + T value(Function<@Nullable B, @Nullable R> bodyMapper, Matcher matcher); /** * Assert the extracted body with a {@link Consumer}. * @since 5.1 */ - T value(Consumer consumer); + T value(Consumer<@Nullable B> consumer); /** * Assert the exchange result with the given {@link Consumer}. @@ -957,7 +976,7 @@ interface BodySpec> { * * @param the body list element type */ - interface ListBodySpec extends BodySpec, ListBodySpec> { + interface ListBodySpec extends BodySpec, ListBodySpec> { /** * Assert the extracted list of values is of the given size. @@ -970,14 +989,14 @@ interface ListBodySpec extends BodySpec, ListBodySpec> { * @param elements the elements to check */ @SuppressWarnings("unchecked") - ListBodySpec contains(E... elements); + ListBodySpec contains(@Nullable E... elements); /** * Assert the extracted list of values doesn't contain the given elements. * @param elements the elements to check */ @SuppressWarnings("unchecked") - ListBodySpec doesNotContain(E... elements); + ListBodySpec doesNotContain(@Nullable E... elements); } @@ -1075,19 +1094,6 @@ default BodyContentSpec json(String expectedJson) { */ JsonPathAssertions jsonPath(String expression); - /** - * Access to response body assertions using a - * JsonPath expression - * to inspect a specific subset of the body. - *

      The JSON path expression can be a parameterized string using - * formatting specifiers as defined in {@link String#format}. - * @param expression the JsonPath expression - * @param args arguments to parameterize the expression - * @deprecated in favor of calling {@link String#formatted(Object...)} upfront - */ - @Deprecated(since = "6.2", forRemoval = true) - JsonPathAssertions jsonPath(String expression, Object... args); - /** * Access to response body assertions using an XPath expression to * inspect a specific subset of the body. diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClientConfigurer.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClientConfigurer.java index bf5bfb43930a..6a3144408983 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClientConfigurer.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WebTestClientConfigurer.java @@ -16,8 +16,9 @@ package org.springframework.test.web.reactive.server; +import org.jspecify.annotations.Nullable; + import org.springframework.http.client.reactive.ClientHttpConnector; -import org.springframework.lang.Nullable; import org.springframework.web.server.adapter.WebHttpHandlerBuilder; /** diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java index c502d883ade6..20460df9d567 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/WiretapConnector.java @@ -23,6 +23,7 @@ import java.util.concurrent.atomic.AtomicReference; import java.util.function.Function; +import org.jspecify.annotations.Nullable; import org.reactivestreams.Publisher; import reactor.core.Scannable; import reactor.core.publisher.Flux; @@ -37,7 +38,6 @@ import org.springframework.http.client.reactive.ClientHttpRequestDecorator; import org.springframework.http.client.reactive.ClientHttpResponse; import org.springframework.http.client.reactive.ClientHttpResponseDecorator; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; /** @@ -62,7 +62,7 @@ class WiretapConnector implements ClientHttpConnector { @Override - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Mono connect(HttpMethod method, URI uri, Function> requestCallback) { @@ -129,11 +129,9 @@ public WiretapClientHttpResponse getResponse() { */ static final class WiretapRecorder { - @Nullable - private final Flux publisher; + private final @Nullable Flux publisher; - @Nullable - private final Flux> publisherNested; + private final @Nullable Flux> publisherNested; private final DataBuffer buffer = DefaultDataBufferFactory.sharedInstance.allocateBuffer(256); @@ -182,7 +180,7 @@ public Publisher> getNestedPublisherTo return this.publisherNested; } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation public Mono getContent() { return Mono.defer(() -> { if (this.content.scan(Scannable.Attr.TERMINATED) == Boolean.TRUE) { @@ -223,8 +221,7 @@ private void handleOnComplete() { */ private static class WiretapClientHttpRequest extends ClientHttpRequestDecorator { - @Nullable - private WiretapRecorder recorder; + private @Nullable WiretapRecorder recorder; public WiretapClientHttpRequest(ClientHttpRequest delegate) { @@ -280,8 +277,7 @@ public Flux getBody() { return Flux.from(this.recorder.getPublisherToUse()); } - @Nullable - public Object getMockServerResult() { + public @Nullable Object getMockServerResult() { return (getDelegate() instanceof MockServerClientHttpResponse mockResponse ? mockResponse.getServerResult() : null); } diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java index 57355a914f81..396446e500fd 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/XpathAssertions.java @@ -24,9 +24,9 @@ import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; -import org.springframework.lang.Nullable; import org.springframework.test.util.XpathExpectationsHelper; import org.springframework.util.Assert; import org.springframework.util.MimeType; diff --git a/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java b/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java index 681359421451..cd0345375923 100644 --- a/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/reactive/server/package-info.java @@ -2,9 +2,7 @@ * Support for testing Spring WebFlux server endpoints via * {@link org.springframework.test.web.reactive.server.WebTestClient}. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.reactive.server; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java index 5582235cfb38..937b2e203bce 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/DefaultMvcResult.java @@ -20,7 +20,8 @@ import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicReference; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.Assert; @@ -45,22 +46,17 @@ class DefaultMvcResult implements MvcResult { private final MockHttpServletResponse mockResponse; - @Nullable - private Object handler; + private @Nullable Object handler; - @Nullable - private HandlerInterceptor[] interceptors; + private HandlerInterceptor @Nullable [] interceptors; - @Nullable - private ModelAndView modelAndView; + private @Nullable ModelAndView modelAndView; - @Nullable - private Exception resolvedException; + private @Nullable Exception resolvedException; private final AtomicReference asyncResult = new AtomicReference<>(RESULT_NONE); - @Nullable - private CountDownLatch asyncDispatchLatch; + private @Nullable CountDownLatch asyncDispatchLatch; /** @@ -87,18 +83,16 @@ public void setHandler(@Nullable Object handler) { } @Override - @Nullable - public Object getHandler() { + public @Nullable Object getHandler() { return this.handler; } - public void setInterceptors(@Nullable HandlerInterceptor... interceptors) { + public void setInterceptors(HandlerInterceptor @Nullable ... interceptors) { this.interceptors = interceptors; } @Override - @Nullable - public HandlerInterceptor[] getInterceptors() { + public HandlerInterceptor @Nullable [] getInterceptors() { return this.interceptors; } @@ -107,8 +101,7 @@ public void setResolvedException(Exception resolvedException) { } @Override - @Nullable - public Exception getResolvedException() { + public @Nullable Exception getResolvedException() { return this.resolvedException; } @@ -117,8 +110,7 @@ public void setModelAndView(@Nullable ModelAndView mav) { } @Override - @Nullable - public ModelAndView getModelAndView() { + public @Nullable ModelAndView getModelAndView() { return this.modelAndView; } @@ -132,13 +124,12 @@ public void setAsyncResult(@Nullable Object asyncResult) { } @Override - public Object getAsyncResult() { + public @Nullable Object getAsyncResult() { return getAsyncResult(-1); } @Override - @SuppressWarnings("NullAway") - public Object getAsyncResult(long timeToWait) { + public @Nullable Object getAsyncResult(long timeToWait) { if (this.mockRequest.getAsyncContext() != null && timeToWait == -1) { long requestTimeout = this.mockRequest.getAsyncContext().getTimeout(); timeToWait = requestTimeout == -1 ? Long.MAX_VALUE : requestTimeout; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java index 7acf46ba1529..2d38806620e7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvc.java @@ -27,9 +27,9 @@ import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpServletResponseWrapper; +import org.jspecify.annotations.Nullable; import org.springframework.beans.Mergeable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockFilterChain; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; @@ -78,11 +78,9 @@ public final class MockMvc { private final ServletContext servletContext; - @Nullable - private RequestBuilder defaultRequestBuilder; + private @Nullable RequestBuilder defaultRequestBuilder; - @Nullable - private Charset defaultResponseCharacterEncoding; + private @Nullable Charset defaultResponseCharacterEncoding; private List defaultResultMatchers = new ArrayList<>(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java index 40403ddf60f7..dd3a48eec41d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MockMvcBuilderSupport.java @@ -21,9 +21,9 @@ import jakarta.servlet.Filter; import jakarta.servlet.ServletException; +import org.jspecify.annotations.Nullable; import org.springframework.core.NestedRuntimeException; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockServletConfig; import org.springframework.web.context.WebApplicationContext; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java index 31b2e15b4721..00cdea90884a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/MvcResult.java @@ -16,7 +16,8 @@ package org.springframework.test.web.servlet; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.web.servlet.FlashMap; @@ -48,30 +49,26 @@ public interface MvcResult { * Return the executed handler. * @return the handler, possibly {@code null} if none were executed */ - @Nullable - Object getHandler(); + @Nullable Object getHandler(); /** * Return interceptors around the handler. * @return interceptors, or {@code null} if none were selected */ - @Nullable - HandlerInterceptor[] getInterceptors(); + HandlerInterceptor @Nullable [] getInterceptors(); /** * Return the {@code ModelAndView} prepared by the handler. * @return a {@code ModelAndView}, or {@code null} if none */ - @Nullable - ModelAndView getModelAndView(); + @Nullable ModelAndView getModelAndView(); /** * Return any exception raised by a handler and successfully resolved * through a {@link HandlerExceptionResolver}. * @return an exception, or {@code null} if none */ - @Nullable - Exception getResolvedException(); + @Nullable Exception getResolvedException(); /** * Return the "output" flash attributes saved during request processing. @@ -88,7 +85,7 @@ public interface MvcResult { * {@link #getAsyncResult(long)} to specify the amount of time to wait. * @throws IllegalStateException if the async result was not set */ - Object getAsyncResult(); + @Nullable Object getAsyncResult(); /** * Get the result of async execution and wait if necessary. @@ -99,6 +96,6 @@ public interface MvcResult { * MockAsyncContext#setTimeout} for more details. * @throws IllegalStateException if the async result was not set */ - Object getAsyncResult(long timeToWait); + @Nullable Object getAsyncResult(long timeToWait); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/ResultMatcher.java b/spring-test/src/main/java/org/springframework/test/web/servlet/ResultMatcher.java index 44aae68e150c..5fbc10aba702 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/ResultMatcher.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/ResultMatcher.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. @@ -57,21 +57,4 @@ public interface ResultMatcher { */ void match(MvcResult result) throws Exception; - - /** - * Static method for matching with an array of result matchers. - * @param matchers the matchers - * @since 5.1 - * @deprecated as of Spring Framework 5.3.10, in favor of - * {@link ResultActions#andExpectAll(ResultMatcher...)} - */ - @Deprecated - static ResultMatcher matchAll(ResultMatcher... matchers) { - return result -> { - for (ResultMatcher matcher : matchers) { - matcher.match(result); - } - }; - } - } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java b/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java index 8b3b6d2d7b03..afea3bfaa4ee 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/TestDispatcherServlet.java @@ -24,8 +24,8 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockAsyncContext; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.util.Assert; @@ -118,8 +118,7 @@ protected DefaultMvcResult getMvcResult(ServletRequest request) { } @Override - @Nullable - protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { + protected @Nullable HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception { HandlerExecutionChain chain = super.getHandler(request); if (chain != null) { DefaultMvcResult mvcResult = getMvcResult(request); @@ -139,8 +138,7 @@ protected void render(ModelAndView mv, HttpServletRequest request, HttpServletRe } @Override - @Nullable - protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, + protected @Nullable ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response, @Nullable Object handler, Exception ex) throws Exception { ModelAndView mav = super.processHandlerException(request, response, handler, ex); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java index 6fc0dbd79aff..d1dd8e3eb1ba 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/AbstractMockHttpServletResponseAssert.java @@ -23,8 +23,8 @@ import org.assertj.core.api.Assertions; import org.assertj.core.api.ByteArrayAssert; import org.assertj.core.api.StringAssert; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.test.json.AbstractJsonContentAssert; @@ -44,8 +44,7 @@ public abstract class AbstractMockHttpServletResponseAssert, ACTUAL> extends AbstractHttpServletResponseAssert { - @Nullable - private final HttpMessageContentConverter contentConverter; + private final @Nullable HttpMessageContentConverter contentConverter; protected AbstractMockHttpServletResponseAssert( @Nullable HttpMessageContentConverter contentConverter, ACTUAL actual, Class selfType) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java index 5a0303def556..67620be77889 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/DefaultMvcTestResult.java @@ -16,7 +16,8 @@ package org.springframework.test.web.servlet.assertj; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.http.HttpMessageContentConverter; import org.springframework.test.web.servlet.MvcResult; @@ -28,14 +29,11 @@ */ final class DefaultMvcTestResult implements MvcTestResult { - @Nullable - private final MvcResult mvcResult; + private final @Nullable MvcResult mvcResult; - @Nullable - private final Exception unresolvedException; + private final @Nullable Exception unresolvedException; - @Nullable - private final HttpMessageContentConverter contentConverter; + private final @Nullable HttpMessageContentConverter contentConverter; DefaultMvcTestResult(@Nullable MvcResult mvcResult, @Nullable Exception unresolvedException, @@ -57,13 +55,11 @@ public MvcResult getMvcResult() { } @Override - @Nullable - public Exception getUnresolvedException() { + public @Nullable Exception getUnresolvedException() { return this.unresolvedException; } - @Nullable - public Exception getResolvedException() { + public @Nullable Exception getResolvedException() { return getMvcResult().getResolvedException(); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java index e97be143bdc7..0f50be392622 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/HandlerResultAssert.java @@ -20,9 +20,9 @@ import org.assertj.core.api.AbstractObjectAssert; import org.assertj.core.api.Assertions; +import org.jspecify.annotations.Nullable; import org.springframework.cglib.core.internal.Function; -import org.springframework.lang.Nullable; import org.springframework.test.util.MethodAssert; import org.springframework.util.ClassUtils; import org.springframework.web.method.HandlerMethod; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java index 309ee6c78a77..9c8cfcc50d03 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MockMvcTester.java @@ -24,10 +24,10 @@ import jakarta.servlet.DispatcherType; import org.assertj.core.api.AssertProvider; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpMethod; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartHttpServletRequest; import org.springframework.test.http.HttpMessageContentConverter; @@ -132,8 +132,7 @@ public final class MockMvcTester { private final MockMvc mockMvc; - @Nullable - private final HttpMessageContentConverter contentConverter; + private final @Nullable HttpMessageContentConverter contentConverter; private MockMvcTester(MockMvc mockMvc, @Nullable HttpMessageContentConverter contentConverter) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java index 73bcebdacccb..c58773b538c2 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/ModelAssert.java @@ -24,8 +24,8 @@ import org.assertj.core.api.AbstractMapAssert; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.validation.AbstractBindingResultAssert; import org.springframework.validation.BindingResult; import org.springframework.validation.BindingResultUtils; @@ -142,8 +142,7 @@ private int getAllErrors() { .map(Errors::getErrorCount).reduce(0, Integer::sum); } - @Nullable - private BindingResult getBindingResult(String name) { + private @Nullable BindingResult getBindingResult(String name) { return BindingResultUtils.getBindingResult(this.actual, name); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java index 20b6e942db15..a32ecffda9fa 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResult.java @@ -17,8 +17,8 @@ package org.springframework.test.web.servlet.assertj; import org.assertj.core.api.AssertProvider; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; @@ -77,7 +77,6 @@ default MockHttpServletResponse getResponse() { * Return the exception that was thrown unexpectedly while processing the * request, if any. */ - @Nullable - Exception getUnresolvedException(); + @Nullable Exception getUnresolvedException(); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java index 55fd9e0ec6ad..18bf3a9aff84 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/MvcTestResultAssert.java @@ -30,8 +30,8 @@ import org.assertj.core.api.MapAssert; import org.assertj.core.error.BasicErrorMessageFactory; import org.assertj.core.internal.Failures; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.http.HttpMessageContentConverter; @@ -204,8 +204,7 @@ public MvcTestResultAssert hasViewName(String viewName) { return this.myself; } - @Nullable - private Throwable getFailure() { + private @Nullable Throwable getFailure() { Exception unresolvedException = this.actual.getUnresolvedException(); if (unresolvedException != null) { return unresolvedException; @@ -213,7 +212,7 @@ private Throwable getFailure() { return this.actual.getMvcResult().getResolvedException(); } - @SuppressWarnings("NullAway") + @SuppressWarnings("NullAway") // Dataflow analysis limitation private ModelAndView getModelAndView() { ModelAndView modelAndView = getMvcResult().getModelAndView(); Assertions.assertThat(modelAndView).as("ModelAndView").isNotNull(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java index 6fe626a51659..49ca44ffecdd 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/assertj/package-info.java @@ -1,9 +1,7 @@ /** * AssertJ support for MockMvc. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.assertj; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java index e387a3b65e89..eaf9dea7a897 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/MockMvcHttpConnector.java @@ -26,6 +26,7 @@ import java.util.function.Function; import jakarta.servlet.http.Cookie; +import org.jspecify.annotations.Nullable; import reactor.core.publisher.Mono; import org.springframework.core.ResolvableType; @@ -43,7 +44,6 @@ import org.springframework.http.codec.multipart.DefaultPartHttpMessageReader; import org.springframework.http.codec.multipart.FilePart; import org.springframework.http.codec.multipart.Part; -import org.springframework.lang.Nullable; import org.springframework.mock.http.client.reactive.MockClientHttpRequest; import org.springframework.mock.http.client.reactive.MockClientHttpResponse; import org.springframework.mock.http.server.reactive.MockServerHttpRequest; @@ -151,7 +151,7 @@ private RequestBuilder adaptRequest( } private AbstractMockHttpServletRequestBuilder initRequestBuilder( - HttpMethod httpMethod, URI uri, MockClientHttpRequest httpRequest, @Nullable byte[] bytes) { + HttpMethod httpMethod, URI uri, MockClientHttpRequest httpRequest, byte @Nullable [] bytes) { String contentType = httpRequest.getHeaders().getFirst(HttpHeaders.CONTENT_TYPE); if (!StringUtils.startsWithIgnoreCase(contentType, "multipart/")) { @@ -266,27 +266,23 @@ public MockHttpServletResponse getResponse() { return this.mvcResult.getResponse(); } - @Nullable @Override - public Object getHandler() { + public @Nullable Object getHandler() { return this.mvcResult.getHandler(); } - @Nullable @Override - public HandlerInterceptor[] getInterceptors() { + public HandlerInterceptor @Nullable [] getInterceptors() { return this.mvcResult.getInterceptors(); } - @Nullable @Override - public ModelAndView getModelAndView() { + public @Nullable ModelAndView getModelAndView() { return this.mvcResult.getModelAndView(); } - @Nullable @Override - public Exception getResolvedException() { + public @Nullable Exception getResolvedException() { return this.mvcResult.getResolvedException(); } @@ -296,12 +292,12 @@ public FlashMap getFlashMap() { } @Override - public Object getAsyncResult() { + public @Nullable Object getAsyncResult() { return this.mvcResult.getAsyncResult(); } @Override - public Object getAsyncResult(long timeToWait) { + public @Nullable Object getAsyncResult(long timeToWait) { return this.mvcResult.getAsyncResult(timeToWait); } 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 cb632fa6e28b..7967512b3fed 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. @@ -19,11 +19,11 @@ import java.util.function.Supplier; import jakarta.servlet.Filter; +import org.jspecify.annotations.Nullable; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.client.reactive.ClientHttpConnector; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.test.web.reactive.server.ExchangeResult; import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.servlet.DispatcherServletCustomizer; @@ -38,6 +38,7 @@ 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 @@ interface ControllerSpec extends MockMvcServerSpec { */ 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 @@ -297,7 +306,7 @@ interface ControllerSpec extends MockMvcServerSpec { * {@link StandaloneMockMvcBuilder#addMappedInterceptors(String[], HandlerInterceptor...)}. */ ControllerSpec mappedInterceptors( - @Nullable String[] pathPatterns, HandlerInterceptor... interceptors); + String @Nullable [] pathPatterns, HandlerInterceptor... interceptors); /** * Set a ContentNegotiationManager. @@ -370,16 +379,6 @@ ControllerSpec mappedInterceptors( */ ControllerSpec patternParser(PathPatternParser parser); - /** - * Whether to match trailing slashes. - *

      This is delegated to - * {@link StandaloneMockMvcBuilder#setUseTrailingSlashPatternMatch(boolean)}. - * @deprecated as of 6.0, see - * {@link PathPatternParser#setMatchOptionalTrailingSeparator(boolean)} - */ - @Deprecated(since = "6.0") - ControllerSpec useTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch); - /** * Configure placeholder values to use. *

      This is delegated to @@ -424,7 +423,7 @@ interface RouterFunctionSpec extends MockMvcServerSpec { * {@link RouterFunctionMockMvcBuilder#addMappedInterceptors(String[], HandlerInterceptor...)}. */ RouterFunctionSpec mappedInterceptors( - @Nullable String[] pathPatterns, HandlerInterceptor... interceptors); + String @Nullable [] pathPatterns, HandlerInterceptor... interceptors); /** * Specify the timeout value for async execution. diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/RouterFunctionMockMvcSpec.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/RouterFunctionMockMvcSpec.java index 5ff3e1dfc2b8..6a53caa81b11 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/RouterFunctionMockMvcSpec.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/RouterFunctionMockMvcSpec.java @@ -16,8 +16,9 @@ package org.springframework.test.web.servlet.client; +import org.jspecify.annotations.Nullable; + import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.RouterFunctionMockMvcBuilder; @@ -58,7 +59,7 @@ public MockMvcWebTestClient.RouterFunctionSpec interceptors(HandlerInterceptor.. } @Override - public MockMvcWebTestClient.RouterFunctionSpec mappedInterceptors(@Nullable String[] pathPatterns, HandlerInterceptor... interceptors) { + public MockMvcWebTestClient.RouterFunctionSpec mappedInterceptors(String @Nullable [] pathPatterns, HandlerInterceptor... interceptors) { this.mockMvcBuilder.addMappedInterceptors(pathPatterns, interceptors); return this; } 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 b7b1f4d44881..d0f6712f11fc 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-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,13 +18,15 @@ import java.util.function.Supplier; +import org.jspecify.annotations.Nullable; + import org.springframework.format.support.FormattingConversionService; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.lang.Nullable; 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; @@ -78,6 +80,12 @@ public StandaloneMockMvcSpec conversionService(FormattingConversionService conve return this; } + @Override + public MockMvcWebTestClient.ControllerSpec apiVersionStrategy(ApiVersionStrategy versionStrategy) { + this.mockMvcBuilder.setApiVersionStrategy(versionStrategy); + return this; + } + @Override public StandaloneMockMvcSpec interceptors(HandlerInterceptor... interceptors) { mappedInterceptors(null, interceptors); @@ -86,7 +94,7 @@ public StandaloneMockMvcSpec interceptors(HandlerInterceptor... interceptors) { @Override public StandaloneMockMvcSpec mappedInterceptors( - @Nullable String[] pathPatterns, HandlerInterceptor... interceptors) { + String @Nullable [] pathPatterns, HandlerInterceptor... interceptors) { this.mockMvcBuilder.addMappedInterceptors(pathPatterns, interceptors); return this; @@ -152,13 +160,6 @@ public StandaloneMockMvcSpec patternParser(PathPatternParser parser) { return this; } - @SuppressWarnings("deprecation") - @Override - public StandaloneMockMvcSpec useTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch) { - this.mockMvcBuilder.setUseTrailingSlashPatternMatch(useTrailingSlashPatternMatch); - return this; - } - @Override public StandaloneMockMvcSpec placeholderValue(String name, String value) { this.mockMvcBuilder.addPlaceholderValue(name, value); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java index 7323682c55dd..ed071659d10b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/client/package-info.java @@ -5,9 +5,7 @@ * handling. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.client; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java index 4c370d5727be..d717ca3c86b3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/HtmlUnitRequestBuilder.java @@ -39,10 +39,10 @@ import org.htmlunit.WebRequest; import org.htmlunit.util.KeyDataPair; import org.htmlunit.util.NameValuePair; +import org.jspecify.annotations.Nullable; import org.springframework.beans.Mergeable; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.mock.web.MockPart; @@ -79,17 +79,13 @@ final class HtmlUnitRequestBuilder implements RequestBuilder, Mergeable { private final WebRequest webRequest; - @Nullable - private String contextPath; + private @Nullable String contextPath; - @Nullable - private RequestBuilder parentBuilder; + private @Nullable RequestBuilder parentBuilder; - @Nullable - private SmartRequestBuilder parentPostProcessor; + private @Nullable SmartRequestBuilder parentPostProcessor; - @Nullable - private RequestPostProcessor forwardPostProcessor; + private @Nullable RequestPostProcessor forwardPostProcessor; /** @@ -234,8 +230,7 @@ private void authType(MockHttpServletRequest request) { } } - @Nullable - private String getHeader(String headerName) { + private @Nullable String getHeader(String headerName) { return this.webRequest.getAdditionalHeaders().get(headerName); } @@ -451,8 +446,7 @@ public HtmlUnitMockHttpServletRequest(ServletContext servletContext, String meth } @Override - @Nullable - public HttpSession getSession(boolean create) { + public @Nullable HttpSession getSession(boolean create) { HttpSession session = super.getSession(false); if (session == null && create) { HtmlUnitMockHttpSession newSession = new HtmlUnitMockHttpSession(this); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java index cab9933e02cc..5a40b8541fc4 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java @@ -17,8 +17,8 @@ package org.springframework.test.web.servlet.htmlunit; import org.htmlunit.WebClient; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.util.Assert; @@ -43,8 +43,7 @@ */ public class MockMvcWebClientBuilder extends MockMvcWebConnectionBuilderSupport { - @Nullable - private WebClient webClient; + private @Nullable WebClient webClient; protected MockMvcWebClientBuilder(MockMvc mockMvc) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java index 165a9c0c5687..c79f231e7101 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java @@ -28,8 +28,8 @@ import org.htmlunit.WebRequest; import org.htmlunit.WebResponse; import org.htmlunit.util.Cookie; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; @@ -63,8 +63,7 @@ public final class MockMvcWebConnection implements WebConnection { private final MockMvc mockMvc; - @Nullable - private final String contextPath; + private final @Nullable String contextPath; private WebClient webClient; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java index 9219e76328df..98757e378737 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/package-info.java @@ -3,9 +3,7 @@ * and HtmlUnit. * @see org.springframework.test.web.servlet.MockMvc */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.htmlunit; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java index 4092859f8fe6..219a5f8ab60d 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java @@ -18,9 +18,9 @@ import org.htmlunit.BrowserVersion; import org.htmlunit.WebClient; +import org.jspecify.annotations.Nullable; import org.openqa.selenium.htmlunit.HtmlUnitDriver; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.MockMvc; import org.springframework.test.web.servlet.htmlunit.MockMvcWebConnectionBuilderSupport; import org.springframework.test.web.servlet.htmlunit.WebRequestMatcher; @@ -49,8 +49,7 @@ */ public class MockMvcHtmlUnitDriverBuilder extends MockMvcWebConnectionBuilderSupport { - @Nullable - private HtmlUnitDriver driver; + private @Nullable HtmlUnitDriver driver; private boolean javascriptEnabled = true; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java index e314c09e6f5f..85aa06c1165f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/package-info.java @@ -4,9 +4,7 @@ * @see org.springframework.test.web.servlet.MockMvc * @see org.openqa.selenium.htmlunit.HtmlUnitDriver */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.htmlunit.webdriver; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/package-info.java index 63a7fb0faa3d..46cbb3de8fc5 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/package-info.java @@ -2,9 +2,7 @@ * Contains server-side support for testing Spring MVC applications. * @see org.springframework.test.web.servlet.MockMvc */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java index 84e7cb47ef4e..67be1e436517 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockHttpServletRequestBuilder.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 jakarta.servlet.ServletRequest; import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpSession; +import org.jspecify.annotations.Nullable; import org.springframework.beans.Mergeable; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -45,16 +46,18 @@ import org.springframework.http.HttpOutputMessage; import org.springframework.http.MediaType; import org.springframework.http.converter.FormHttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.RequestBuilder; import org.springframework.util.Assert; import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.MultiValueMap; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; +import org.springframework.web.client.ApiVersionFormatter; +import org.springframework.web.client.ApiVersionInserter; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.support.WebApplicationContextUtils; import org.springframework.web.servlet.DispatcherServlet; @@ -82,39 +85,29 @@ public abstract class AbstractMockHttpServletRequestBuilder headers = new LinkedMultiValueMap<>(); @@ -128,6 +121,10 @@ public abstract class AbstractMockHttpServletRequestBuilder locales = new ArrayList<>(); + private @Nullable Object version; + + private @Nullable ApiVersionInserter versionInserter; + private final Map requestAttributes = new LinkedHashMap<>(); private final Map sessionAttributes = new LinkedHashMap<>(); @@ -161,7 +158,7 @@ public B uri(URI uri) { /** * Specify the URI for the request using a URI template and URI variables. */ - public B uri(String uriTemplate, Object... uriVariables) { + public B uri(String uriTemplate, @Nullable Object... uriVariables) { return updateUri(initUri(uriTemplate, uriVariables), uriTemplate); } @@ -171,7 +168,7 @@ private B updateUri(URI uri, @Nullable String uriTemplate) { return self(); } - private static URI initUri(String uri, Object[] vars) { + private static URI initUri(String uri, @Nullable Object[] vars) { Assert.notNull(uri, "'uri' must not be null"); Assert.isTrue(uri.isEmpty() || uri.startsWith("/") || uri.startsWith("http://") || uri.startsWith("https://"), () -> "'uri' should start with a path or be a complete HTTP URI: " + uri); @@ -479,6 +476,34 @@ public B locale(@Nullable Locale locale) { return self(); } + /** + * Set an API version for the request. The version is inserted into the + * request by the {@link #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 the inserter, e.g. through an + * {@link ApiVersionFormatter}. + * @since 7.0 + */ + public B apiVersion(Object version) { + this.version = version; + return self(); + } + + /** + * Configure an {@link ApiVersionInserter} to abstract how an API version + * specified via {@link #apiVersion(Object)} is inserted into the request. + * An inserter may typically be set once (more centrally) via + * {@link org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder#defaultRequest(RequestBuilder)}, or + * {@link org.springframework.test.web.servlet.setup.ConfigurableMockMvcBuilder#apiVersionInserter(ApiVersionInserter)}. + * @param versionInserter the inserter to use + * @since 7.0 + */ + public B apiVersionInserter(ApiVersionInserter versionInserter) { + this.versionInserter = versionInserter; + return self(); + } + /** * Set a request attribute. * @param name the attribute name @@ -672,6 +697,14 @@ public Object merge(@Nullable Object parent) { } } + if (this.version == null) { + this.version = parentBuilder.version; + } + + if (this.versionInserter == null) { + this.versionInserter = parentBuilder.versionInserter; + } + for (Map.Entry entry : parentBuilder.requestAttributes.entrySet()) { String attributeName = entry.getKey(); if (!this.requestAttributes.containsKey(attributeName)) { @@ -710,7 +743,15 @@ private boolean containsCookie(Cookie cookie) { */ @Override public final MockHttpServletRequest buildRequest(ServletContext servletContext) { - Assert.notNull(this.uri, "'uri' is required"); + + URI uri = this.uri; + Assert.notNull(uri, "'uri' is required"); + + if (this.version != null) { + Assert.state(this.versionInserter != null, "No ApiVersionInserter"); + uri = this.versionInserter.insertVersion(this.version, uri); + } + MockHttpServletRequest request = createServletRequest(servletContext); request.setAsyncSupported(true); @@ -718,17 +759,17 @@ public final MockHttpServletRequest buildRequest(ServletContext servletContext) request.setUriTemplate(this.uriTemplate); - String requestUri = this.uri.getRawPath(); + String requestUri = uri.getRawPath(); request.setRequestURI(requestUri); - if (this.uri.getScheme() != null) { - request.setScheme(this.uri.getScheme()); + if (uri.getScheme() != null) { + request.setScheme(uri.getScheme()); } - if (this.uri.getHost() != null) { - request.setServerName(this.uri.getHost()); + if (uri.getHost() != null) { + request.setServerName(uri.getHost()); } - if (this.uri.getPort() != -1) { - request.setServerPort(this.uri.getPort()); + if (uri.getPort() != -1) { + request.setServerPort(uri.getPort()); } updatePathRequestProperties(request, requestUri); @@ -750,6 +791,13 @@ public final MockHttpServletRequest buildRequest(ServletContext servletContext) request.setContent(this.content); request.setContentType(this.contentType); + if (this.version != null) { + Assert.state(this.versionInserter != null, "No ApiVersionInserter"); + HttpHeaders httpHeaders = new HttpHeaders(); + this.versionInserter.insertVersion(this.version, httpHeaders); + httpHeaders.forEach((name, values) -> values.forEach(value -> this.headers.add(name, value))); + } + this.headers.forEach((name, values) -> { for (Object value : values) { request.addHeader(name, value); @@ -763,7 +811,7 @@ public final MockHttpServletRequest buildRequest(ServletContext servletContext) request.addHeader(HttpHeaders.CONTENT_LENGTH, this.content.length); } - String query = this.uri.getRawQuery(); + String query = uri.getRawQuery(); if (!this.queryParams.isEmpty()) { String str = UriComponentsBuilder.newInstance().queryParams(this.queryParams).build().encode().getQuery(); query = StringUtils.hasLength(query) ? (query + "&" + str) : str; @@ -771,7 +819,7 @@ public final MockHttpServletRequest buildRequest(ServletContext servletContext) if (query != null) { request.setQueryString(query); } - addRequestParams(request, UriComponentsBuilder.fromUri(this.uri).build().getQueryParams()); + addRequestParams(request, UriComponentsBuilder.fromUri(uri).build().getQueryParams()); this.parameters.forEach((name, values) -> { for (String value : values) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockMultipartHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockMultipartHttpServletRequestBuilder.java index 9b9de5adb5cc..3fb7a6d1270b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockMultipartHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/AbstractMockMultipartHttpServletRequestBuilder.java @@ -27,10 +27,10 @@ import jakarta.servlet.ServletContext; import jakarta.servlet.http.Part; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartHttpServletRequest; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java index 4ec50f72c0c3..b4944fa5c73f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.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,11 +23,11 @@ import java.util.Map; import jakarta.servlet.http.Cookie; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpHeaders; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpSession; import org.springframework.test.web.servlet.MockMvc; @@ -75,7 +75,7 @@ public MockHttpServletRequestBuilder uri(URI uri) { } @Override - public MockHttpServletRequestBuilder uri(String uriTemplate, Object... uriVariables) { + public MockHttpServletRequestBuilder uri(String uriTemplate, @Nullable Object... uriVariables) { return super.uri(uriTemplate, uriVariables); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java index 4817e7fb4905..d55417c1127f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMultipartHttpServletRequestBuilder.java @@ -27,10 +27,10 @@ import jakarta.servlet.ServletContext; import jakarta.servlet.http.Part; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartHttpServletRequest; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java index 00c0c54ceb21..abf479391090 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.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,6 +19,7 @@ import java.net.URI; import jakarta.servlet.DispatcherType; +import org.jspecify.annotations.Nullable; import org.springframework.http.HttpMethod; import org.springframework.mock.web.MockHttpServletRequest; @@ -52,7 +53,7 @@ public abstract class MockMvcRequestBuilders { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder get(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder get(String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.GET).uri(uriTemplate, uriVariables); } @@ -70,7 +71,7 @@ public static MockHttpServletRequestBuilder get(URI uri) { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder post(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder post(String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.POST).uri(uriTemplate, uriVariables); } @@ -88,7 +89,7 @@ public static MockHttpServletRequestBuilder post(URI uri) { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder put(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder put(String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.PUT).uri(uriTemplate, uriVariables); } @@ -106,7 +107,7 @@ public static MockHttpServletRequestBuilder put(URI uri) { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder patch(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder patch(String uriTemplate,@Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.PATCH).uri(uriTemplate, uriVariables); } @@ -124,7 +125,7 @@ public static MockHttpServletRequestBuilder patch(URI uri) { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder delete(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder delete(String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.DELETE).uri(uriTemplate, uriVariables); } @@ -142,7 +143,7 @@ public static MockHttpServletRequestBuilder delete(URI uri) { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder options(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder options(String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS).uri(uriTemplate, uriVariables); } @@ -161,7 +162,7 @@ public static MockHttpServletRequestBuilder options(URI uri) { * @param uriVariables zero or more URI variables * @since 4.1 */ - public static MockHttpServletRequestBuilder head(String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder head(String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(HttpMethod.HEAD).uri(uriTemplate, uriVariables); } @@ -180,7 +181,7 @@ public static MockHttpServletRequestBuilder head(URI uri) { * @param uriTemplate a URI template; the resulting URI will be encoded * @param uriVariables zero or more URI variables */ - public static MockHttpServletRequestBuilder request(HttpMethod method, String uriTemplate, Object... uriVariables) { + public static MockHttpServletRequestBuilder request(HttpMethod method, String uriTemplate, @Nullable Object... uriVariables) { return new MockHttpServletRequestBuilder(method).uri(uriTemplate, uriVariables); } @@ -213,7 +214,7 @@ public static MockHttpServletRequestBuilder request(String httpMethod, URI uri) * @param uriVariables zero or more URI variables * @since 5.0 */ - public static MockMultipartHttpServletRequestBuilder multipart(String uriTemplate, Object... uriVariables) { + public static MockMultipartHttpServletRequestBuilder multipart(String uriTemplate, @Nullable Object... uriVariables) { MockMultipartHttpServletRequestBuilder builder = new MockMultipartHttpServletRequestBuilder(); builder.uri(uriTemplate, uriVariables); return builder; @@ -227,7 +228,7 @@ public static MockMultipartHttpServletRequestBuilder multipart(String uriTemplat * @param uriVariables zero or more URI variables * @since 5.3.22 */ - public static MockMultipartHttpServletRequestBuilder multipart(HttpMethod httpMethod, String uriTemplate, Object... uriVariables) { + public static MockMultipartHttpServletRequestBuilder multipart(HttpMethod httpMethod, String uriTemplate, @Nullable Object... uriVariables) { MockMultipartHttpServletRequestBuilder builder = new MockMultipartHttpServletRequestBuilder(httpMethod); builder.uri(uriTemplate, uriVariables); return builder; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/package-info.java index 35a802a763d6..46d993389918 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/package-info.java @@ -4,9 +4,7 @@ * {@link org.springframework.test.web.servlet.request.MockMvcRequestBuilders} * to gain access to instances of those implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.request; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java index fa3c9dcf7631..61182c4f6e1b 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ContentResultMatchers.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. diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/FlashAttributeResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/FlashAttributeResultMatchers.java index ce66b0449906..5c711971e294 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/FlashAttributeResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/FlashAttributeResultMatchers.java @@ -17,8 +17,8 @@ package org.springframework.test.web.servlet.result; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.ResultMatcher; import static org.hamcrest.MatcherAssert.assertThat; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java index 01e722fffd8b..7222894da3ec 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java @@ -22,8 +22,8 @@ import org.hamcrest.Matcher; import org.hamcrest.MatcherAssert; import org.hamcrest.core.StringStartsWith; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; @@ -47,8 +47,7 @@ public class JsonPathResultMatchers { private final JsonPathExpectationsHelper jsonPathHelper; - @Nullable - private String prefix; + private @Nullable String prefix; /** diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultHandlers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultHandlers.java index f03d6c6ef305..7edc99343580 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultHandlers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultHandlers.java @@ -23,8 +23,8 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultHandler; import org.springframework.util.CollectionUtils; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java index a3392535bbbf..7c800058e2e3 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.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. @@ -21,8 +21,8 @@ import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.util.AntPathMatcher; import org.springframework.web.util.UriComponentsBuilder; @@ -98,7 +98,7 @@ public static ResultMatcher forwardedUrl(@Nullable String expectedUrl) { * @param uriVars zero or more URI variables to populate the template * @see UriComponentsBuilder#fromUriString(String) */ - public static ResultMatcher forwardedUrlTemplate(String urlTemplate, Object... uriVars) { + public static ResultMatcher forwardedUrlTemplate(String urlTemplate, @Nullable Object... uriVars) { String uri = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(uriVars).encode().toUriString(); return forwardedUrl(uri); } @@ -137,7 +137,7 @@ public static ResultMatcher redirectedUrl(String expectedUrl) { * @param uriVars zero or more URI variables to populate the template * @see UriComponentsBuilder#fromUriString(String) */ - public static ResultMatcher redirectedUrlTemplate(String urlTemplate, Object... uriVars) { + public static ResultMatcher redirectedUrlTemplate(String urlTemplate, @Nullable Object... uriVars) { String uri = UriComponentsBuilder.fromUriString(urlTemplate).buildAndExpand(uriVars).encode().toUriString(); return redirectedUrl(uri); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ModelResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ModelResultMatchers.java index 599c05f09b5a..68edeec65213 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/ModelResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/ModelResultMatchers.java @@ -17,8 +17,8 @@ package org.springframework.test.web.servlet.result; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.ui.ModelMap; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/PrintingResultHandler.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/PrintingResultHandler.java index 61952f737389..c155895175ca 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/PrintingResultHandler.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/PrintingResultHandler.java @@ -24,10 +24,10 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import org.jspecify.annotations.Nullable; import org.springframework.core.style.ToStringCreator; import org.springframework.http.HttpHeaders; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; @@ -172,7 +172,7 @@ protected void printAsyncResult(MvcResult result) throws Exception { /** * Print the handler. */ - protected void printHandler(@Nullable Object handler, @Nullable HandlerInterceptor[] interceptors) + protected void printHandler(@Nullable Object handler, HandlerInterceptor @Nullable [] interceptors) throws Exception { if (handler == null) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/RequestResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/RequestResultMatchers.java index 2cd6f3b9fc28..415d07373c22 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/RequestResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/RequestResultMatchers.java @@ -21,8 +21,8 @@ import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.util.Assert; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java index 65fc10f94726..d7555f5c55d7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java @@ -146,16 +146,6 @@ public ResultMatcher isProcessing() { return matcher(HttpStatus.PROCESSING); } - /** - * Assert the response status code is {@code HttpStatus.CHECKPOINT} (103). - * @see #isEarlyHints() - * @deprecated in favor of {@link #isEarlyHints()} - */ - @Deprecated(since = "6.0.5") - public ResultMatcher isCheckpoint() { - return isEarlyHints(); - } - /** * Assert the response status code is {@code HttpStatus.EARLY_HINTS} (103). * @since 6.0.5 @@ -255,16 +245,6 @@ public ResultMatcher isFound() { return matcher(HttpStatus.FOUND); } - /** - * Assert the response status code is {@code HttpStatus.MOVED_TEMPORARILY} (302). - * @see #isFound() - * @deprecated in favor of {@link #isFound()} - */ - @Deprecated - public ResultMatcher isMovedTemporarily() { - return matcher(HttpStatus.MOVED_TEMPORARILY); - } - /** * Assert the response status code is {@code HttpStatus.SEE_OTHER} (303). */ @@ -279,15 +259,6 @@ public ResultMatcher isNotModified() { return matcher(HttpStatus.NOT_MODIFIED); } - /** - * Assert the response status code is {@code HttpStatus.USE_PROXY} (305). - * @deprecated matching the deprecation of {@code HttpStatus.USE_PROXY} - */ - @Deprecated - public ResultMatcher isUseProxy() { - return matcher(HttpStatus.USE_PROXY); - } - /** * Assert the response status code is {@code HttpStatus.TEMPORARY_REDIRECT} (307). */ @@ -401,16 +372,6 @@ public ResultMatcher isPayloadTooLarge() { return matcher(HttpStatus.PAYLOAD_TOO_LARGE); } - /** - * Assert the response status code is {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE} (413). - * @see #isPayloadTooLarge() - * @deprecated matching the deprecation of {@code HttpStatus.REQUEST_ENTITY_TOO_LARGE} - */ - @Deprecated - public ResultMatcher isRequestEntityTooLarge() { - return matcher(HttpStatus.REQUEST_ENTITY_TOO_LARGE); - } - /** * Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414). * @since 4.1 @@ -419,16 +380,6 @@ public ResultMatcher isUriTooLong() { return matcher(HttpStatus.URI_TOO_LONG); } - /** - * Assert the response status code is {@code HttpStatus.REQUEST_URI_TOO_LONG} (414). - * @see #isUriTooLong() - * @deprecated matching the deprecation of {@code HttpStatus.REQUEST_URI_TOO_LONG} - */ - @Deprecated - public ResultMatcher isRequestUriTooLong() { - return matcher(HttpStatus.REQUEST_URI_TOO_LONG); - } - /** * Assert the response status code is {@code HttpStatus.UNSUPPORTED_MEDIA_TYPE} (415). */ @@ -457,33 +408,6 @@ public ResultMatcher isIAmATeapot() { return matcher(HttpStatus.valueOf(418)); } - /** - * Assert the response status code is {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE} (419). - * @deprecated matching the deprecation of {@code HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE} - */ - @Deprecated - public ResultMatcher isInsufficientSpaceOnResource() { - return matcher(HttpStatus.INSUFFICIENT_SPACE_ON_RESOURCE); - } - - /** - * Assert the response status code is {@code HttpStatus.METHOD_FAILURE} (420). - * @deprecated matching the deprecation of {@code HttpStatus.METHOD_FAILURE} - */ - @Deprecated - public ResultMatcher isMethodFailure() { - return matcher(HttpStatus.METHOD_FAILURE); - } - - /** - * Assert the response status code is {@code HttpStatus.DESTINATION_LOCKED} (421). - * @deprecated matching the deprecation of {@code HttpStatus.DESTINATION_LOCKED} - */ - @Deprecated - public ResultMatcher isDestinationLocked() { - return matcher(HttpStatus.DESTINATION_LOCKED); - } - /** * Assert the response status code is {@code HttpStatus.UNPROCESSABLE_ENTITY} (422). */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java index 3925b5ccab67..bc7107a43341 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/XpathResultMatchers.java @@ -21,10 +21,10 @@ import javax.xml.xpath.XPathExpressionException; import org.hamcrest.Matcher; +import org.jspecify.annotations.Nullable; import org.w3c.dom.Node; import org.w3c.dom.NodeList; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.util.XpathExpectationsHelper; import org.springframework.test.web.servlet.ResultMatcher; @@ -85,8 +85,7 @@ public ResultMatcher nodeList(Matcher matcher) { /** * Get the response encoding if explicitly defined in the response, {@code null} otherwise. */ - @Nullable - private String getDefinedEncoding(MockHttpServletResponse response) { + private @Nullable String getDefinedEncoding(MockHttpServletResponse response) { return (response.isCharset() ? response.getCharacterEncoding() : null); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/package-info.java index 67057faef948..4c1c13105c8e 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/package-info.java @@ -4,9 +4,7 @@ * and {@link org.springframework.test.web.servlet.result.MockMvcResultHandlers} * to access instances of those implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.result; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 1180d5a4a42b..2189ad5dd697 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. @@ -26,8 +26,8 @@ import jakarta.servlet.Filter; import jakarta.servlet.ServletContext; import jakarta.servlet.ServletException; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockServletConfig; import org.springframework.test.web.servlet.DispatcherServletCustomizer; import org.springframework.test.web.servlet.MockMvc; @@ -36,10 +36,12 @@ 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,11 +64,11 @@ public abstract class AbstractMockMvcBuilder private final List filters = new ArrayList<>(); - @Nullable - private RequestBuilder defaultRequestBuilder; + private @Nullable ApiVersionInserter apiVersionInserter; - @Nullable - private Charset defaultResponseCharacterEncoding; + private @Nullable RequestBuilder defaultRequestBuilder; + + private @Nullable Charset defaultResponseCharacterEncoding; private final List globalResultMatchers = new ArrayList<>(); @@ -108,6 +110,12 @@ public T addFilter( return self(); } + @Override + public T apiVersionInserter(ApiVersionInserter versionInserter) { + this.apiVersionInserter = versionInserter; + return self(); + } + @Override public final T defaultRequest(RequestBuilder requestBuilder) { this.defaultRequestBuilder = requestBuilder; @@ -191,11 +199,20 @@ public final MockMvc build() { 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 40de7774f21d..7afd093c88e1 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. @@ -23,13 +23,14 @@ import jakarta.servlet.DispatcherType; import jakarta.servlet.Filter; import jakarta.servlet.FilterConfig; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.DispatcherServletCustomizer; 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 @@ T addFilter( Filter filter, @Nullable String filterName, Map 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/MockMvcConfigurer.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurer.java index b7707efde915..bc00d50a1a96 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurer.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurer.java @@ -16,7 +16,8 @@ package org.springframework.test.web.servlet.setup; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.web.context.WebApplicationContext; @@ -60,8 +61,7 @@ default void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { * @return a post processor to be applied to every request performed * through the {@code MockMvc} instance. */ - @Nullable - default RequestPostProcessor beforeMockMvcCreated( + default @Nullable RequestPostProcessor beforeMockMvcCreated( ConfigurableMockMvcBuilder builder, WebApplicationContext context) { return null; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurerAdapter.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurerAdapter.java index be680aef9b98..2e18d12005a9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurerAdapter.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcConfigurerAdapter.java @@ -16,7 +16,8 @@ package org.springframework.test.web.servlet.setup; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.web.context.WebApplicationContext; @@ -33,8 +34,7 @@ public void afterConfigurerAdded(ConfigurableMockMvcBuilder builder) { } @Override - @Nullable - public RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, WebApplicationContext cxt) { + public @Nullable RequestPostProcessor beforeMockMvcCreated(ConfigurableMockMvcBuilder builder, WebApplicationContext cxt) { return null; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.java index f07720d4abc3..a5af570c6677 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/MockMvcFilterDecorator.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,8 +32,8 @@ import jakarta.servlet.ServletRequest; import jakarta.servlet.ServletResponse; import jakarta.servlet.http.HttpServletRequest; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockFilterConfig; import org.springframework.mock.web.MockFilterRegistration; import org.springframework.mock.web.MockServletContext; @@ -58,11 +58,9 @@ final class MockMvcFilterDecorator implements Filter { private final Filter delegate; - @Nullable - private final Function filterConfigInitializer; + private final @Nullable Function<@Nullable ServletContext, FilterConfig> filterConfigInitializer; - @Nullable - private final EnumSet dispatcherTypes; + private final @Nullable EnumSet dispatcherTypes; private final boolean hasPatterns; @@ -105,7 +103,7 @@ public MockMvcFilterDecorator( this.hasPatterns = initPatterns(urlPatterns); } - private static Function getFilterConfigInitializer( + private static Function<@Nullable ServletContext, FilterConfig> getFilterConfigInitializer( Filter delegate, @Nullable String filterName, @Nullable Map initParams) { String className = delegate.getClass().getName(); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/RouterFunctionMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/RouterFunctionMockMvcBuilder.java index 079b363a2695..7f3ea90a6932 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/RouterFunctionMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/RouterFunctionMockMvcBuilder.java @@ -23,13 +23,13 @@ import java.util.function.Supplier; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.InitializingBean; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockServletContext; import org.springframework.util.Assert; import org.springframework.web.accept.ContentNegotiationManager; @@ -80,17 +80,13 @@ public class RouterFunctionMockMvcBuilder extends AbstractMockMvcBuilder mappedInterceptors = new ArrayList<>(); - @Nullable - private List handlerExceptionResolvers; + private @Nullable List handlerExceptionResolvers; - @Nullable - private Long asyncRequestTimeout; + private @Nullable Long asyncRequestTimeout; - @Nullable - private List viewResolvers; + private @Nullable List viewResolvers; - @Nullable - private PathPatternParser patternParser; + private @Nullable PathPatternParser patternParser; private Supplier handlerMappingFactory = RouterFunctionMapping::new; @@ -124,7 +120,7 @@ public RouterFunctionMockMvcBuilder addInterceptors(HandlerInterceptor... interc /** * Add interceptors mapped to a set of path patterns. */ - public RouterFunctionMockMvcBuilder addMappedInterceptors(@Nullable String[] pathPatterns, + public RouterFunctionMockMvcBuilder addMappedInterceptors(String @Nullable [] pathPatterns, HandlerInterceptor... interceptors) { for (HandlerInterceptor interceptor : interceptors) { @@ -192,9 +188,9 @@ public RouterFunctionMockMvcBuilder setAsyncRequestTimeout(long timeout) { } /** - * Enable URL path matching with parsed - * {@link org.springframework.web.util.pattern.PathPattern PathPatterns} - * instead of String pattern matching with a {@link org.springframework.util.PathMatcher}. + * Configure the parser to use for + * {@link org.springframework.web.util.pattern.PathPattern PathPatterns}. + *

      By default, this is a default instance of {@link PathPatternParser}. * @param parser the parser to use */ public RouterFunctionMockMvcBuilder setPatternParser(@Nullable PathPatternParser parser) { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/SharedHttpSessionConfigurer.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/SharedHttpSessionConfigurer.java index d12bca60d824..defde9465bbe 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/SharedHttpSessionConfigurer.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/SharedHttpSessionConfigurer.java @@ -17,8 +17,8 @@ package org.springframework.test.web.servlet.setup; import jakarta.servlet.http.HttpSession; +import org.jspecify.annotations.Nullable; -import org.springframework.lang.Nullable; import org.springframework.test.web.servlet.request.RequestPostProcessor; import org.springframework.web.context.WebApplicationContext; @@ -44,8 +44,7 @@ */ public class SharedHttpSessionConfigurer implements MockMvcConfigurer { - @Nullable - private HttpSession session; + private @Nullable HttpSession session; @Override 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 4c68fffef1cb..2759335aedf1 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. @@ -25,6 +25,7 @@ import java.util.function.Supplier; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; @@ -35,12 +36,13 @@ import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.FormattingConversionService; import org.springframework.http.converter.HttpMessageConverter; -import org.springframework.lang.Nullable; import org.springframework.mock.web.MockServletContext; +import org.springframework.util.PathMatcher; 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; @@ -91,8 +93,7 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder controllers; - @Nullable - private List controllerAdvice; + private @Nullable List controllerAdvice; private List> messageConverters = new ArrayList<>(); @@ -102,40 +103,29 @@ public class StandaloneMockMvcBuilder extends AbstractMockMvcBuilder mappedInterceptors = new ArrayList<>(); - @Nullable - private Validator validator; + private @Nullable Validator validator; - @Nullable - private ContentNegotiationManager contentNegotiationManager; + private @Nullable ContentNegotiationManager contentNegotiationManager; - @Nullable - private FormattingConversionService conversionService; + private @Nullable FormattingConversionService conversionService; - @Nullable - private List handlerExceptionResolvers; + private @Nullable ApiVersionStrategy versionStrategy; - @Nullable - private Long asyncRequestTimeout; + private @Nullable List handlerExceptionResolvers; - @Nullable - private List viewResolvers; + private @Nullable Long asyncRequestTimeout; + + private @Nullable List viewResolvers; private LocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); - @Nullable - private FlashMapManager flashMapManager; + private @Nullable FlashMapManager flashMapManager; private boolean preferPathMatcher = false; - @Nullable - private PathPatternParser patternParser; - - private boolean useSuffixPatternMatch = false; - - private boolean useTrailingSlashPatternMatch = false; + private @Nullable PathPatternParser patternParser; - @Nullable - private Boolean removeSemicolonContent; + private @Nullable Boolean removeSemicolonContent; private final Map placeholderValues = new HashMap<>(); @@ -202,6 +192,15 @@ public StandaloneMockMvcBuilder setConversionService(FormattingConversionService return this; } + /** + * Set the {@link ApiVersionStrategy} to use when mapping requests. + * @since 7.0 + */ + public StandaloneMockMvcBuilder setApiVersionStrategy(@Nullable ApiVersionStrategy versionStrategy) { + this.versionStrategy = versionStrategy; + return this; + } + /** * Add interceptors mapped to all incoming requests. */ @@ -214,7 +213,7 @@ public StandaloneMockMvcBuilder addInterceptors(HandlerInterceptor... intercepto * Add interceptors mapped to a set of path patterns. */ public StandaloneMockMvcBuilder addMappedInterceptors( - @Nullable String[] pathPatterns, HandlerInterceptor... interceptors) { + String @Nullable [] pathPatterns, HandlerInterceptor... interceptors) { for (HandlerInterceptor interceptor : interceptors) { this.mappedInterceptors.add(new MappedInterceptor(pathPatterns, null, interceptor)); @@ -311,9 +310,9 @@ public StandaloneMockMvcBuilder setFlashMapManager(FlashMapManager flashMapManag } /** - * Enable URL path matching with parsed - * {@link org.springframework.web.util.pattern.PathPattern PathPatterns} - * instead of String pattern matching with a {@link org.springframework.util.PathMatcher}. + * Configure the parser to use for + * {@link org.springframework.web.util.pattern.PathPattern PathPatterns}. + *

      By default, this is a default instance of {@link PathPatternParser}. * @param parser the parser to use * @since 5.3 */ @@ -323,38 +322,15 @@ public StandaloneMockMvcBuilder setPatternParser(@Nullable PathPatternParser par return this; } - /** - * Whether to use suffix pattern match (".*") when matching patterns to - * requests. If enabled a method mapped to "/users" also matches to "/users.*". - *

      The default value is {@code false}. - * @deprecated as of 5.2.4. See class-level note in - * {@link RequestMappingHandlerMapping} on the deprecation of path extension - * config options. - */ - @Deprecated - public StandaloneMockMvcBuilder setUseSuffixPatternMatch(boolean useSuffixPatternMatch) { - this.useSuffixPatternMatch = useSuffixPatternMatch; - this.preferPathMatcher |= useSuffixPatternMatch; - return this; - } - - /** - * Whether to match to URLs irrespective of the presence of a trailing slash. - * If enabled a method mapped to "/users" also matches to "/users/". - * @deprecated as of 6.0, see - * {@link PathPatternParser#setMatchOptionalTrailingSeparator(boolean)} - */ - @Deprecated(since = "6.0") - public StandaloneMockMvcBuilder setUseTrailingSlashPatternMatch(boolean useTrailingSlashPatternMatch) { - this.useTrailingSlashPatternMatch = useTrailingSlashPatternMatch; - return this; - } - /** * Set if ";" (semicolon) content should be stripped from the request URI. The value, * if provided, is in turn set on * {@link org.springframework.web.util.UrlPathHelper#setRemoveSemicolonContent(boolean)}. + * @deprecated use of {@link PathMatcher} and {@link UrlPathHelper} is deprecated + * for use at runtime in web modules in favor of parsed patterns with + * {@link PathPatternParser}. */ + @Deprecated(since = "7.0", forRemoval = true) public StandaloneMockMvcBuilder setRemoveSemicolonContent(boolean removeSemicolonContent) { this.removeSemicolonContent = removeSemicolonContent; return this; @@ -431,8 +407,6 @@ private void registerMvcSingletons(StubWebApplicationContext wac) { wac.addBeans(initViewResolvers(wac)); wac.addBean(DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME, this.localeResolver); - wac.addBean(DispatcherServlet.THEME_RESOLVER_BEAN_NAME, - new org.springframework.web.servlet.theme.FixedThemeResolver()); wac.addBean(DispatcherServlet.REQUEST_TO_VIEW_NAME_TRANSLATOR_BEAN_NAME, new DefaultRequestToViewNameTranslator()); @@ -469,7 +443,7 @@ protected Map extendMvcSingletons(@Nullable ServletContext servl /** Using the MVC Java configuration as the starting point for the "standalone" setup. */ private class StandaloneConfiguration extends WebMvcConfigurationSupport { - @SuppressWarnings("deprecation") + @SuppressWarnings("removal") public RequestMappingHandlerMapping getHandlerMapping( FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) { @@ -478,7 +452,6 @@ public RequestMappingHandlerMapping getHandlerMapping( handlerMapping.setEmbeddedValueResolver(new StaticStringValueResolver(placeholderValues)); if (patternParser == null && preferPathMatcher) { handlerMapping.setPatternParser(null); - handlerMapping.setUseSuffixPatternMatch(useSuffixPatternMatch); if (removeSemicolonContent != null) { UrlPathHelper pathHelper = new UrlPathHelper(); pathHelper.setRemoveSemicolonContent(removeSemicolonContent); @@ -488,7 +461,9 @@ public RequestMappingHandlerMapping getHandlerMapping( else if (patternParser != null) { handlerMapping.setPatternParser(patternParser); } - handlerMapping.setUseTrailingSlashMatch(useTrailingSlashPatternMatch); + if (versionStrategy != null) { + handlerMapping.setApiVersionStrategy(versionStrategy); + } handlerMapping.setOrder(0); handlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider)); return handlerMapping; diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StaticViewResolver.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StaticViewResolver.java index 87c4bc37bfdc..60dd37dda67a 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StaticViewResolver.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StaticViewResolver.java @@ -18,7 +18,8 @@ import java.util.Locale; -import org.springframework.lang.Nullable; +import org.jspecify.annotations.Nullable; + import org.springframework.web.servlet.View; import org.springframework.web.servlet.ViewResolver; @@ -37,8 +38,7 @@ public StaticViewResolver(View view) { } @Override - @Nullable - public View resolveViewName(String viewName, Locale locale) { + public @Nullable View resolveViewName(String viewName, Locale locale) { return this.view; } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java index 8e8f6476b62d..46f5fd87a393 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/StubWebApplicationContext.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,6 +24,7 @@ import java.util.Set; import jakarta.servlet.ServletContext; +import org.jspecify.annotations.Nullable; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; @@ -47,7 +48,6 @@ import org.springframework.core.env.StandardEnvironment; import org.springframework.core.io.Resource; import org.springframework.core.io.support.ResourcePatternResolver; -import org.springframework.lang.Nullable; import org.springframework.util.ClassUtils; import org.springframework.util.ObjectUtils; import org.springframework.web.context.WebApplicationContext; @@ -130,8 +130,7 @@ public long getStartupDate() { } @Override - @Nullable - public ApplicationContext getParent() { + public @Nullable ApplicationContext getParent() { return null; } @@ -169,7 +168,7 @@ public T getBean(String name, Class requiredType) throws BeansException { } @Override - public Object getBean(String name, Object... args) throws BeansException { + public Object getBean(String name, @Nullable Object @Nullable ... args) throws BeansException { return this.beanFactory.getBean(name, args); } @@ -179,7 +178,7 @@ public T getBean(Class requiredType) throws BeansException { } @Override - public T getBean(Class requiredType, Object... args) throws BeansException { + public T getBean(Class requiredType, @Nullable Object @Nullable ... args) throws BeansException { return this.beanFactory.getBean(requiredType, args); } @@ -219,14 +218,12 @@ public boolean isTypeMatch(String name, Class typeToMatch) throws NoSuchBeanD } @Override - @Nullable - public Class getType(String name) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name) throws NoSuchBeanDefinitionException { return this.beanFactory.getType(name); } @Override - @Nullable - public Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { + public @Nullable Class getType(String name, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { return this.beanFactory.getType(name, allowFactoryBeanInit); } @@ -310,16 +307,14 @@ public Map getBeansWithAnnotation(Class an } @Override - @Nullable - public A findAnnotationOnBean(String beanName, Class annotationType) + public @Nullable A findAnnotationOnBean(String beanName, Class annotationType) throws NoSuchBeanDefinitionException{ return this.beanFactory.findAnnotationOnBean(beanName, annotationType); } @Override - @Nullable - public A findAnnotationOnBean( + public @Nullable A findAnnotationOnBean( String beanName, Class annotationType, boolean allowFactoryBeanInit) throws NoSuchBeanDefinitionException { @@ -340,8 +335,7 @@ public Set findAllAnnotationsOnBean( //--------------------------------------------------------------------- @Override - @Nullable - public BeanFactory getParentBeanFactory() { + public @Nullable BeanFactory getParentBeanFactory() { return null; } @@ -356,13 +350,12 @@ public boolean containsLocalBean(String name) { //--------------------------------------------------------------------- @Override - @Nullable - public String getMessage(String code, @Nullable Object[] args, @Nullable String defaultMessage, Locale locale) { + public @Nullable String getMessage(String code, Object @Nullable [] args, @Nullable String defaultMessage, Locale locale) { return this.messageSource.getMessage(code, args, defaultMessage, locale); } @Override - public String getMessage(String code, @Nullable Object[] args, Locale locale) throws NoSuchMessageException { + public String getMessage(String code, Object @Nullable [] args, Locale locale) throws NoSuchMessageException { return this.messageSource.getMessage(code, args, locale); } @@ -377,8 +370,7 @@ public String getMessage(MessageSourceResolvable resolvable, Locale locale) thro //--------------------------------------------------------------------- @Override - @Nullable - public ClassLoader getClassLoader() { + public @Nullable ClassLoader getClassLoader() { return ClassUtils.getDefaultClassLoader(); } @@ -426,7 +418,7 @@ public T createBean(Class beanClass) { return BeanUtils.instantiateClass(beanClass); } - @Deprecated + @Deprecated(since = "6.1") @Override public Object createBean(Class beanClass, int autowireMode, boolean dependencyCheck) { return BeanUtils.instantiateClass(beanClass); @@ -461,14 +453,12 @@ public Object resolveBeanByName(String name, DependencyDescriptor descriptor) th } @Override - @Nullable - public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) { + public @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName) { throw new UnsupportedOperationException("Dependency resolution not supported"); } @Override - @Nullable - public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, + public @Nullable Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName, @Nullable Set autowiredBeanNames, @Nullable TypeConverter typeConverter) { throw new UnsupportedOperationException("Dependency resolution not supported"); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/package-info.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/package-info.java index f030410ea827..3d1b982464e9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/package-info.java @@ -3,9 +3,7 @@ * Use {@link org.springframework.test.web.servlet.setup.MockMvcBuilders} * to access to instances of those implementations. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.web.servlet.setup; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; diff --git a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/StatusResultMatchersDsl.kt b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/StatusResultMatchersDsl.kt index d07bb1e6fb21..195178bec7a2 100644 --- a/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/StatusResultMatchersDsl.kt +++ b/spring-test/src/main/kotlin/org/springframework/test/web/servlet/result/StatusResultMatchersDsl.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. @@ -114,15 +114,6 @@ class StatusResultMatchersDsl internal constructor (private val actions: ResultA actions.andExpect(matchers.isProcessing()) } - /** - * @see isEarlyHints - */ - @Deprecated("use isEarlyHints() instead", replaceWith= ReplaceWith("isEarlyHints()")) - fun isCheckpoint() { - @Suppress("DEPRECATION") - actions.andExpect(matchers.isCheckpoint()) - } - /** * @see StatusResultMatchers.isEarlyHints * @since 6.0.5 diff --git a/spring-test/src/test/java/org/springframework/mock/http/server/reactive/MockServerHttpRequestTests.java b/spring-test/src/test/java/org/springframework/mock/http/server/reactive/MockServerHttpRequestTests.java index bd541fe1a0cd..0a02563193e4 100644 --- a/spring-test/src/test/java/org/springframework/mock/http/server/reactive/MockServerHttpRequestTests.java +++ b/spring-test/src/test/java/org/springframework/mock/http/server/reactive/MockServerHttpRequestTests.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.mock.http.server.reactive; +import java.net.URI; import java.util.Arrays; import java.util.stream.Stream; @@ -27,7 +28,6 @@ import org.springframework.http.HttpCookie; import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; import org.springframework.web.util.UriComponentsBuilder; import static org.assertj.core.api.Assertions.assertThat; @@ -74,15 +74,12 @@ void httpMethodNotNullOrEmpty(ThrowingCallable callable) { .withMessageContaining("HTTP method is required."); } - @SuppressWarnings("deprecation") static Stream> httpMethodNotNullOrEmpty() { String uriTemplate = "/foo bar?a=b"; + URI uri = UriComponentsBuilder.fromUriString(uriTemplate).build().toUri(); return Stream.of( - named("null HttpMethod, URI", () -> MockServerHttpRequest.method(null, UriComponentsBuilder.fromUriString(uriTemplate).build("")).build()), - named("null HttpMethod, uriTemplate", () -> MockServerHttpRequest.method((HttpMethod) null, uriTemplate).build()), - named("null String, uriTemplate", () -> MockServerHttpRequest.method((String) null, uriTemplate).build()), - named("empty String, uriTemplate", () -> MockServerHttpRequest.method("", uriTemplate).build()), - named("blank String, uriTemplate", () -> MockServerHttpRequest.method(" ", uriTemplate).build()) + named("null HttpMethod, URI", () -> MockServerHttpRequest.method(null, uri).build()), + named("null HttpMethod, uriTemplate", () -> MockServerHttpRequest.method(null, uriTemplate).build()) ); } 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 006c68745f88..6ab8ba41c80a 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 @@ void contentTypeHeaderUTF8() { 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/mock/web/MockHttpServletResponseTests.java b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java index 68c23013bdab..069f38cf89de 100644 --- a/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java +++ b/spring-test/src/test/java/org/springframework/mock/web/MockHttpServletResponseTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2024 the original author or authors. + * Copyright 2002-2025 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -26,23 +26,19 @@ import jakarta.servlet.http.Cookie; import jakarta.servlet.http.HttpServletResponse; +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.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.web.util.WebUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; +import static org.assertj.core.api.Assertions.assertThatIllegalStateException; import static org.assertj.core.api.InstanceOfAssertFactories.type; -import static org.assertj.core.api.SoftAssertions.assertSoftly; -import static org.springframework.http.HttpHeaders.CONTENT_LANGUAGE; -import static org.springframework.http.HttpHeaders.CONTENT_LENGTH; -import static org.springframework.http.HttpHeaders.CONTENT_TYPE; -import static org.springframework.http.HttpHeaders.LAST_MODIFIED; -import static org.springframework.http.HttpHeaders.LOCATION; -import static org.springframework.http.HttpHeaders.SET_COOKIE; /** * Tests for {@link MockHttpServletResponse}. @@ -55,598 +51,633 @@ * @author Brian Clozel * @author Sebastien Deleuze * @author Vedran Pavic - * @since 19.02.2006 */ class MockHttpServletResponseTests { private MockHttpServletResponse response = new MockHttpServletResponse(); - - @ParameterizedTest // gh-26488 - @ValueSource(strings = { - CONTENT_TYPE, - CONTENT_LENGTH, - CONTENT_LANGUAGE, - SET_COOKIE, - "enigma" - }) - void addHeaderWithNullValue(String headerName) { - response.addHeader(headerName, null); - assertThat(response.containsHeader(headerName)).isFalse(); - } - - @ParameterizedTest // gh-26488 - @ValueSource(strings = { - CONTENT_TYPE, - CONTENT_LENGTH, - CONTENT_LANGUAGE, - SET_COOKIE, - "enigma" - }) - void setHeaderWithNullValue(String headerName) { - response.setHeader(headerName, null); - assertThat(response.containsHeader(headerName)).isFalse(); - } - - @ParameterizedTest - @ValueSource(strings = { - CONTENT_TYPE, - CONTENT_LANGUAGE, - "X-Test-Header" - }) - void removeHeaderIfNullValue(String headerName) { - response.addHeader(headerName, "test"); - response.setHeader(headerName, null); - assertThat(response.containsHeader(headerName)).isFalse(); - } - - @Test // gh-26493 - void setLocaleWithNullValue() { - assertThat(response.getLocale()).isEqualTo(Locale.getDefault()); - response.setLocale(null); - assertThat(response.getLocale()).isEqualTo(Locale.getDefault()); - } - - @Test - void setContentType() { - String contentType = "test/plain"; - response.setContentType(contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); - } - - @Test - void setContentTypeUTF8() { - String contentType = "test/plain;charset=UTF-8"; - response.setContentType(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - } - - @Test - void contentTypeHeader() { - String contentType = "test/plain"; - response.setHeader(CONTENT_TYPE, contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); - - response = new MockHttpServletResponse(); - response.addHeader(CONTENT_TYPE, contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); - } - - @Test - void contentTypeHeaderUTF8() { - String contentType = "test/plain;charset=UTF-8"; - response.setHeader(CONTENT_TYPE, contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - - response = new MockHttpServletResponse(); - response.addHeader(CONTENT_TYPE, contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - } - - @Test // SPR-12677 - void contentTypeHeaderWithMoreComplexCharsetSyntax() { - String contentType = "test/plain;charset=\"utf-8\";foo=\"charset=bar\";foocharset=bar;foo=bar"; - response.setHeader(CONTENT_TYPE, contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - - response = new MockHttpServletResponse(); - response.addHeader(CONTENT_TYPE, contentType); - assertThat(response.getContentType()).isEqualTo(contentType); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo(contentType); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - } - - @Test // gh-25281 - void contentLanguageHeaderWithSingleValue() { - String contentLanguage = "it"; - response.setHeader(CONTENT_LANGUAGE, contentLanguage); - assertSoftly(softly -> { - softly.assertThat(response.getHeader(CONTENT_LANGUAGE)).isEqualTo(contentLanguage); - softly.assertThat(response.getLocale()).isEqualTo(Locale.ITALIAN); - }); - } - - @Test // gh-25281 - void contentLanguageHeaderWithMultipleValues() { - String contentLanguage = "it, en"; - response.setHeader(CONTENT_LANGUAGE, contentLanguage); - assertSoftly(softly -> { - softly.assertThat(response.getHeader(CONTENT_LANGUAGE)).isEqualTo(contentLanguage); - softly.assertThat(response.getLocale()).isEqualTo(Locale.ITALIAN); - }); - } - - @Test - void setContentTypeThenCharacterEncoding() { - response.setContentType("test/plain"); - response.setCharacterEncoding("UTF-8"); - assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - } - - @Test - void setCharacterEncodingThenContentType() { - response.setCharacterEncoding("UTF-8"); - response.setContentType("test/plain"); - assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - } - - @Test - void setCharacterEncodingNull() { - response.setContentType("test/plain"); - response.setCharacterEncoding("UTF-8"); - assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); - response.setCharacterEncoding((String) null); - assertThat(response.getContentType()).isEqualTo("test/plain"); - assertThat(response.getHeader(CONTENT_TYPE)).isEqualTo("test/plain"); - assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); - } - - @Test - void defaultCharacterEncoding() { - assertThat(response.isCharset()).isFalse(); - assertThat(response.getContentType()).isNull(); - assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); - - response.setDefaultCharacterEncoding("UTF-8"); - assertThat(response.isCharset()).isFalse(); - assertThat(response.getContentType()).isNull(); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - - response.setContentType("text/plain;charset=UTF-16"); - assertThat(response.isCharset()).isTrue(); - assertThat(response.getContentType()).isEqualTo("text/plain;charset=UTF-16"); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-16"); - - response.reset(); - assertThat(response.isCharset()).isFalse(); - assertThat(response.getContentType()).isNull(); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - - response.setCharacterEncoding("FOXTROT"); - assertThat(response.isCharset()).isTrue(); - assertThat(response.getContentType()).isNull(); - assertThat(response.getCharacterEncoding()).isEqualTo("FOXTROT"); - - response.setDefaultCharacterEncoding("ENIGMA"); - assertThat(response.getCharacterEncoding()).isEqualTo("FOXTROT"); - } - - @Test - void contentLength() { - response.setContentLength(66); - assertThat(response.getContentLength()).isEqualTo(66); - assertThat(response.getHeader(CONTENT_LENGTH)).isEqualTo("66"); - } - - @Test - void contentLengthHeader() { - response.addHeader(CONTENT_LENGTH, "66"); - assertThat(response.getContentLength()).isEqualTo(66); - assertThat(response.getHeader(CONTENT_LENGTH)).isEqualTo("66"); - } - - @Test - void contentLengthIntHeader() { - response.addIntHeader(CONTENT_LENGTH, 66); - assertThat(response.getContentLength()).isEqualTo(66); - assertThat(response.getHeader(CONTENT_LENGTH)).isEqualTo("66"); - } - - @Test - void httpHeaderNameCasingIsPreserved() { - final String headerName = "Header1"; - response.addHeader(headerName, "value1"); - Collection responseHeaders = response.getHeaderNames(); - assertThat(responseHeaders).containsExactly(headerName); - } - - @Test - void cookies() { - Cookie cookie = new MockCookie("foo", "bar"); - cookie.setPath("/path"); - cookie.setDomain("example.com"); - cookie.setMaxAge(0); - cookie.setSecure(true); - cookie.setHttpOnly(true); - cookie.setAttribute("Partitioned", ""); - - response.addCookie(cookie); - - assertThat(response.getHeader(SET_COOKIE)).isEqualTo(("foo=bar; Path=/path; Domain=example.com; " + - "Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; " + - "Secure; HttpOnly; Partitioned")); - } - - @Test - void servletOutputStreamCommittedWhenBufferSizeExceeded() throws IOException { - assertThat(response.isCommitted()).isFalse(); - response.getOutputStream().write('X'); - assertThat(response.isCommitted()).isFalse(); - int size = response.getBufferSize(); - response.getOutputStream().write(new byte[size]); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getContentAsByteArray()).hasSize((size + 1)); - } - - @Test - void servletOutputStreamCommittedOnFlushBuffer() throws IOException { - assertThat(response.isCommitted()).isFalse(); - response.getOutputStream().write('X'); - assertThat(response.isCommitted()).isFalse(); - response.flushBuffer(); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getContentAsByteArray()).hasSize(1); - } - - @Test - void servletWriterCommittedWhenBufferSizeExceeded() throws IOException { - assertThat(response.isCommitted()).isFalse(); - response.getWriter().write("X"); - assertThat(response.isCommitted()).isFalse(); - int size = response.getBufferSize(); - char[] data = new char[size]; - Arrays.fill(data, 'p'); - response.getWriter().write(data); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getContentAsByteArray()).hasSize((size + 1)); - } - - @Test - void servletOutputStreamCommittedOnOutputStreamFlush() throws IOException { - assertThat(response.isCommitted()).isFalse(); - response.getOutputStream().write('X'); - assertThat(response.isCommitted()).isFalse(); - response.getOutputStream().flush(); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getContentAsByteArray()).hasSize(1); - } - - @Test - void servletWriterCommittedOnWriterFlush() throws IOException { - assertThat(response.isCommitted()).isFalse(); - response.getWriter().write("X"); - assertThat(response.isCommitted()).isFalse(); - response.getWriter().flush(); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getContentAsByteArray()).hasSize(1); - } - - @Test // SPR-16683 - void servletWriterCommittedOnWriterClose() throws IOException { - assertThat(response.isCommitted()).isFalse(); - response.getWriter().write("X"); - assertThat(response.isCommitted()).isFalse(); - response.getWriter().close(); - assertThat(response.isCommitted()).isTrue(); - assertThat(response.getContentAsByteArray()).hasSize(1); - } - - @Test // gh-23219 - void contentAsUtf8() throws IOException { - String content = "Příliš žluťoučký kůň úpěl ďábelské ódy"; - response.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); - assertThat(response.getContentAsString(StandardCharsets.UTF_8)).isEqualTo(content); - } - - @Test - void servletWriterAutoFlushedForChar() throws IOException { - response.getWriter().write('X'); - assertThat(response.getContentAsString()).isEqualTo("X"); - } - - @Test - void servletWriterAutoFlushedForCharArray() throws IOException { - response.getWriter().write("XY".toCharArray()); - assertThat(response.getContentAsString()).isEqualTo("XY"); - } - - @Test - void servletWriterAutoFlushedForString() throws IOException { - response.getWriter().write("X"); - assertThat(response.getContentAsString()).isEqualTo("X"); - } - - @Test - void sendRedirect() throws IOException { - String redirectUrl = "/redirect"; - response.sendRedirect(redirectUrl); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_MOVED_TEMPORARILY); - assertThat(response.getHeader(LOCATION)).isEqualTo(redirectUrl); - assertThat(response.getRedirectedUrl()).isEqualTo(redirectUrl); - assertThat(response.isCommitted()).isTrue(); - } - - @Test - void locationHeaderUpdatesGetRedirectedUrl() { - String redirectUrl = "/redirect"; - response.setHeader(LOCATION, redirectUrl); - assertThat(response.getRedirectedUrl()).isEqualTo(redirectUrl); - } - - @Test - void setDateHeader() { - response.setDateHeader(LAST_MODIFIED, 1437472800000L); - assertThat(response.getHeader(LAST_MODIFIED)).isEqualTo("Tue, 21 Jul 2015 10:00:00 GMT"); - } - - @Test - void addDateHeader() { - response.addDateHeader(LAST_MODIFIED, 1437472800000L); - response.addDateHeader(LAST_MODIFIED, 1437472801000L); - assertThat(response.getHeaders(LAST_MODIFIED)).containsExactly( - "Tue, 21 Jul 2015 10:00:00 GMT", "Tue, 21 Jul 2015 10:00:01 GMT"); - } - - @Test - void getDateHeader() { - long time = 1437472800000L; - response.setDateHeader(LAST_MODIFIED, time); - assertThat(response.getHeader(LAST_MODIFIED)).isEqualTo("Tue, 21 Jul 2015 10:00:00 GMT"); - assertThat(response.getDateHeader(LAST_MODIFIED)).isEqualTo(time); - } - - @Test - void getInvalidDateHeader() { - response.setHeader(LAST_MODIFIED, "invalid"); - assertThat(response.getHeader(LAST_MODIFIED)).isEqualTo("invalid"); - assertThatIllegalArgumentException().isThrownBy(() -> response.getDateHeader(LAST_MODIFIED)); - } - - @Test // SPR-16160 - void getNonExistentDateHeader() { - assertThat(response.getHeader(LAST_MODIFIED)).isNull(); - assertThat(response.getDateHeader(LAST_MODIFIED)).isEqualTo(-1); - } - - @Test // SPR-10414 - void modifyStatusAfterSendError() throws IOException { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - response.setStatus(HttpServletResponse.SC_OK); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); - } - - @Test // SPR-10414 - void modifyStatusMessageAfterSendError() throws IOException { - response.sendError(HttpServletResponse.SC_NOT_FOUND); - response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); - assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); - } - - /** - * @since 5.1.10 - */ - @Test - void setCookieHeader() { - response.setHeader(SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax"); - assertNumCookies(1); - assertPrimarySessionCookie("123"); - - // Setting the Set-Cookie header a 2nd time should overwrite the previous value - response.setHeader(SET_COOKIE, "SESSION=999; Path=/; Secure; HttpOnly; SameSite=Lax"); - assertNumCookies(1); - assertPrimarySessionCookie("999"); - } - - /** - * @since 5.1.11 - */ - @Test - void setCookieHeaderWithMaxAgeAndExpiresAttributes() { - String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; - String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=" + expiryDate + "; Secure; HttpOnly; SameSite=Lax"; - response.setHeader(SET_COOKIE, cookieValue); - assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); - - assertNumCookies(1); - assertThat(response.getCookies()[0]).isInstanceOf(MockCookie.class); - MockCookie mockCookie = (MockCookie) response.getCookies()[0]; - assertThat(mockCookie.getMaxAge()).isEqualTo(100); - assertThat(mockCookie.getExpires()).isEqualTo(ZonedDateTime.parse(expiryDate, DateTimeFormatter.RFC_1123_DATE_TIME)); - } - - /** - * @since 5.1.12 - */ - @Test - void setCookieHeaderWithZeroExpiresAttribute() { - String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0"; - response.setHeader(SET_COOKIE, cookieValue); - assertNumCookies(1); - String header = response.getHeader(SET_COOKIE); - assertThat(header).isNotEqualTo(cookieValue); - // We don't assert the actual Expires value since it is based on the current time. - assertThat(header).startsWith("SESSION=123; Path=/; Max-Age=100; Expires="); - } - - @Test - void addCookieHeader() { - response.addHeader(SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax"); - assertNumCookies(1); - assertPrimarySessionCookie("123"); - - // Adding a 2nd cookie header should result in 2 cookies. - response.addHeader(SET_COOKIE, "SESSION=999; Path=/; Secure; HttpOnly; SameSite=Lax"); - assertNumCookies(2); - assertPrimarySessionCookie("123"); - assertCookieValues("123", "999"); - } - - /** - * @since 5.1.11 - */ - @Test - void addCookieHeaderWithMaxAgeAndExpiresAttributes() { - String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; - String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=" + expiryDate + "; Secure; HttpOnly; SameSite=Lax"; - response.addHeader(SET_COOKIE, cookieValue); - assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); - - assertNumCookies(1); - assertThat(response.getCookies()[0]).isInstanceOf(MockCookie.class); - MockCookie mockCookie = (MockCookie) response.getCookies()[0]; - assertThat(mockCookie.getMaxAge()).isEqualTo(100); - assertThat(mockCookie.getExpires()).isEqualTo(ZonedDateTime.parse(expiryDate, DateTimeFormatter.RFC_1123_DATE_TIME)); - } - - /** - * @since 5.1.12 - */ - @Test - void addCookieHeaderWithMaxAgeAndZeroExpiresAttributes() { - String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0"; - response.addHeader(SET_COOKIE, cookieValue); - assertNumCookies(1); - String header = response.getHeader(SET_COOKIE); - assertThat(header).isNotEqualTo(cookieValue); - // We don't assert the actual Expires value since it is based on the current time. - assertThat(header).startsWith("SESSION=123; Path=/; Max-Age=100; Expires="); - } - - /** - * @since 5.2.14 - */ - @Test - void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { - String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; - String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; - response.addHeader(SET_COOKIE, cookieValue); - assertThat(response.getHeader(SET_COOKIE)).isEqualTo(cookieValue); - - assertNumCookies(1); - assertThat(response.getCookies()[0]).isInstanceOf(MockCookie.class); - MockCookie mockCookie = (MockCookie) response.getCookies()[0]; - assertThat(mockCookie.getName()).isEqualTo("SESSION"); - assertThat(mockCookie.getValue()).isEqualTo("123"); - assertThat(mockCookie.getPath()).isEqualTo("/"); - assertThat(mockCookie.getMaxAge()).isEqualTo(-1); - assertThat(mockCookie.getExpires()).isEqualTo(ZonedDateTime.parse(expiryDate, DateTimeFormatter.RFC_1123_DATE_TIME)); - } - - @Test - void addCookie() { - MockCookie mockCookie = new MockCookie("SESSION", "123"); - mockCookie.setPath("/"); - mockCookie.setDomain("example.com"); - mockCookie.setMaxAge(0); - mockCookie.setSecure(true); - mockCookie.setHttpOnly(true); - mockCookie.setSameSite("Lax"); - - response.addCookie(mockCookie); - - assertNumCookies(1); - assertThat(response.getHeader(SET_COOKIE)).isEqualTo(("SESSION=123; Path=/; Domain=example.com; Max-Age=0; " + - "Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax")); - - // Adding a 2nd Cookie should result in 2 Cookies. - response.addCookie(new MockCookie("SESSION", "999")); - assertNumCookies(2); - assertCookieValues("123", "999"); - } - - private void assertNumCookies(int expected) { - assertThat(this.response.getCookies()).hasSize(expected); - } - - private void assertCookieValues(String... expected) { - assertThat(response.getCookies()).extracting(Cookie::getValue).containsExactly(expected); - } - - @SuppressWarnings("removal") - private void assertPrimarySessionCookie(String expectedValue) { - Cookie cookie = this.response.getCookie("SESSION"); - assertThat(cookie).asInstanceOf(type(MockCookie.class)).satisfies(mockCookie -> { + @Nested + class CharacterEncodingTests { + + @Test + void isoShouldBeDefault() { + assertThat(response.isCharset()).isFalse(); + assertThat(response.getContentType()).isNull(); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Test + void shouldSetDefault() { + response.setDefaultCharacterEncoding("UTF-8"); + assertThat(response.isCharset()).isFalse(); + assertThat(response.getContentType()).isNull(); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test + void shouldResetToDefault() { + response.setDefaultCharacterEncoding("UTF-8"); + response.setCharacterEncoding(WebUtils.DEFAULT_CHARACTER_ENCODING); + + response.reset(); + assertThat(response.isCharset()).isFalse(); + assertThat(response.getContentType()).isNull(); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test + void setDefaultShouldNotChangeEncoding() { + response.setCharacterEncoding("UTF-16"); + assertThat(response.isCharset()).isTrue(); + assertThat(response.getContentType()).isNull(); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-16"); + + response.setDefaultCharacterEncoding("UTF-8"); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-16"); + } + + @Test + void shouldSetEncodingWithContentType() { + String contentType = "text/plain;charset=UTF-8"; + response.setContentType(contentType); + assertThat(response.isCharset()).isTrue(); + assertThat(response.getContentType()).isEqualTo(contentType); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test + void shouldSetUtf8EncodingForJson() { + String contentType = "application/json"; + response.setContentType(contentType); + assertThat(response.isCharset()).isFalse(); + assertThat(response.getContentType()).isEqualTo(contentType); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test // SPR-12677 + void shouldSetEncodingWithComplexContentTypeSyntax() { + String contentType = "test/plain;charset=\"utf-8\";foo=\"charset=bar\";foocharset=bar;foo=bar"; + response.setHeader(HttpHeaders.CONTENT_TYPE, contentType); + assertThat(response.getContentType()).isEqualTo(contentType); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(contentType); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test + void setContentTypeThenCharacterEncoding() { + response.setContentType("test/plain"); + response.setCharacterEncoding("UTF-8"); + assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test + void setCharacterEncodingThenContentType() { + response.setCharacterEncoding("UTF-8"); + response.setContentType("test/plain"); + assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + } + + @Test + void setCharacterEncodingNull() { + response.setContentType("test/plain"); + response.setCharacterEncoding("UTF-8"); + assertThat(response.getContentType()).isEqualTo("test/plain;charset=UTF-8"); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("test/plain;charset=UTF-8"); + response.setCharacterEncoding((String) null); + assertThat(response.getContentType()).isEqualTo("test/plain"); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("test/plain"); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Test // gh-25501 + void resetResponseShouldResetCharset() { + assertThat(response.getContentType()).isNull(); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + assertThat(response.isCharset()).isFalse(); + response.setCharacterEncoding("UTF-8"); + assertThat(response.isCharset()).isTrue(); + assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); + response.setContentType("text/plain"); + assertThat(response.getContentType()).isEqualTo("text/plain;charset=UTF-8"); + String contentTypeHeader = response.getHeader(HttpHeaders.CONTENT_TYPE); + assertThat(contentTypeHeader).isEqualTo("text/plain;charset=UTF-8"); + + response.reset(); + + assertThat(response.getContentType()).isNull(); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + assertThat(response.isCharset()).isFalse(); + // Do not invoke setCharacterEncoding() since that sets the charset flag to true. + // response.setCharacterEncoding("UTF-8"); + response.setContentType("text/plain"); + assertThat(response.isCharset()).isFalse(); // should still be false + assertThat(response.getContentType()).isEqualTo("text/plain"); + contentTypeHeader = response.getHeader(HttpHeaders.CONTENT_TYPE); + assertThat(contentTypeHeader).isEqualTo("text/plain"); + } + + } + + + @Nested + class HeadersTests { + + @ParameterizedTest // gh-26488 + @ValueSource(strings = { + HttpHeaders.CONTENT_TYPE, + HttpHeaders.CONTENT_LENGTH, + HttpHeaders.CONTENT_LANGUAGE, + HttpHeaders.SET_COOKIE, + "X-Test" + }) + void addHeaderWithNullValueShouldHaveNoEffect(String headerName) { + response.addHeader(headerName, null); + assertThat(response.containsHeader(headerName)).isFalse(); + } + + @Test + void addHeaderWithNullNameShouldHaveNoEffect() { + response.addHeader(null, "test"); + assertThat(response.getHeaderNames()).isEmpty(); + } + + @ParameterizedTest // gh-26488 + @ValueSource(strings = { + HttpHeaders.CONTENT_TYPE, + HttpHeaders.CONTENT_LENGTH, + HttpHeaders.CONTENT_LANGUAGE, + HttpHeaders.SET_COOKIE, + "X-Test" + }) + void setHeaderWithNullValueShouldHaveNoEffect(String headerName) { + response.setHeader(headerName, null); + assertThat(response.containsHeader(headerName)).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = { + HttpHeaders.CONTENT_LANGUAGE, + "X-Test-Header" + }) + void setHeaderWithNullValueShouldRemoveHeader(String headerName) { + response.addHeader(headerName, "test"); + response.setHeader(headerName, null); + assertThat(response.containsHeader(headerName)).isFalse(); + } + + @Test + void shouldSetContentType() { + String contentType = "text/plain"; + response.setContentType(contentType); + assertThat(response.getContentType()).isEqualTo(contentType); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(contentType); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Test + void shouldSetContentTypeHeader() { + String contentType = "text/plain"; + response.setHeader(HttpHeaders.CONTENT_TYPE, contentType); + assertThat(response.getContentType()).isEqualTo(contentType); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(contentType); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Test + void shouldAddContentTypeHeader() { + String contentType = "text/plain"; + response.addHeader(HttpHeaders.CONTENT_TYPE, contentType); + assertThat(response.getContentType()).isEqualTo(contentType); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo(contentType); + assertThat(response.getCharacterEncoding()).isEqualTo(WebUtils.DEFAULT_CHARACTER_ENCODING); + } + + @Test + void setContentTypeWithNullValueShouldRemoveHeader() { + response.setContentType("application/json"); + response.setContentType(null); + assertThat(response.containsHeader("Content-Type")).isFalse(); + assertThat(response.getContentType()).isNull(); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + } + + @Test + void setContentTypeHeaderWithNullValueShouldRemoveHeader() { + response.setContentType("application/json"); + response.setHeader(HttpHeaders.CONTENT_TYPE, null); + assertThat(response.containsHeader("Content-Type")).isFalse(); + assertThat(response.getContentType()).isNull(); + assertThat(response.getHeader(HttpHeaders.CONTENT_TYPE)).isNull(); + } + + @Test // gh-25281 + void contentLanguageHeaderWithSingleValue() { + String contentLanguage = "it"; + response.setHeader(HttpHeaders.CONTENT_LANGUAGE, contentLanguage); + assertThat(response.getHeader(HttpHeaders.CONTENT_LANGUAGE)).isEqualTo(contentLanguage); + assertThat(response.getLocale()).isEqualTo(Locale.ITALIAN); + } + + @Test // gh-25281 + void contentLanguageHeaderWithMultipleValues() { + String contentLanguage = "it, en"; + response.setHeader(HttpHeaders.CONTENT_LANGUAGE, contentLanguage); + assertThat(response.getHeader(HttpHeaders.CONTENT_LANGUAGE)).isEqualTo(contentLanguage); + assertThat(response.getLocale()).isEqualTo(Locale.ITALIAN); + } + + @Test // gh-34488 + void shouldAddMultipleContentLanguage() { + response.addHeader(HttpHeaders.CONTENT_LANGUAGE, "en"); + response.addHeader(HttpHeaders.CONTENT_LANGUAGE, "fr"); + assertThat(response.getHeaders(HttpHeaders.CONTENT_LANGUAGE)).contains("en", "fr"); + assertThat(response.getLocale()).isEqualTo(Locale.ENGLISH); + } + + @Test + void contentLengthSetsHeader() { + response.setContentLength(66); + assertThat(response.getContentLength()).isEqualTo(66); + assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo("66"); + } + + @Test + void contentLengthHeaderSetsLength() { + response.addHeader(HttpHeaders.CONTENT_LENGTH, "66"); + assertThat(response.getContentLength()).isEqualTo(66); + assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo("66"); + } + + @Test + void contentLengthIntHeader() { + response.addIntHeader(HttpHeaders.CONTENT_LENGTH, 66); + assertThat(response.getContentLength()).isEqualTo(66); + assertThat(response.getHeader(HttpHeaders.CONTENT_LENGTH)).isEqualTo("66"); + } + + @Test + void httpHeaderNameCasingIsPreserved() { + final String headerName = "Header1"; + response.addHeader(headerName, "value1"); + Collection responseHeaders = response.getHeaderNames(); + assertThat(responseHeaders).containsExactly(headerName); + } + + @Test + void setDateHeader() { + response.setDateHeader(HttpHeaders.LAST_MODIFIED, 1437472800000L); + assertThat(response.getHeader(HttpHeaders.LAST_MODIFIED)).isEqualTo("Tue, 21 Jul 2015 10:00:00 GMT"); + } + + @Test + void addDateHeader() { + response.addDateHeader(HttpHeaders.LAST_MODIFIED, 1437472800000L); + response.addDateHeader(HttpHeaders.LAST_MODIFIED, 1437472801000L); + assertThat(response.getHeaders(HttpHeaders.LAST_MODIFIED)).containsExactly( + "Tue, 21 Jul 2015 10:00:00 GMT", "Tue, 21 Jul 2015 10:00:01 GMT"); + } + + @Test + void getDateHeader() { + long time = 1437472800000L; + response.setDateHeader(HttpHeaders.LAST_MODIFIED, time); + assertThat(response.getHeader(HttpHeaders.LAST_MODIFIED)).isEqualTo("Tue, 21 Jul 2015 10:00:00 GMT"); + assertThat(response.getDateHeader(HttpHeaders.LAST_MODIFIED)).isEqualTo(time); + } + + @Test + void getInvalidDateHeader() { + response.setHeader(HttpHeaders.LAST_MODIFIED, "invalid"); + assertThat(response.getHeader(HttpHeaders.LAST_MODIFIED)).isEqualTo("invalid"); + assertThatIllegalArgumentException().isThrownBy(() -> response.getDateHeader(HttpHeaders.LAST_MODIFIED)); + } + + @Test // SPR-16160 + void getNonExistentDateHeader() { + assertThat(response.getHeader(HttpHeaders.LAST_MODIFIED)).isNull(); + assertThat(response.getDateHeader(HttpHeaders.LAST_MODIFIED)).isEqualTo(-1); + } + + } + + @Nested + class CookiesTests { + + @Test + void cookies() { + Cookie cookie = new MockCookie("foo", "bar"); + cookie.setPath("/path"); + cookie.setDomain("example.com"); + cookie.setMaxAge(0); + cookie.setSecure(true); + cookie.setHttpOnly(true); + cookie.setAttribute("Partitioned", ""); + + response.addCookie(cookie); + + assertThat(response.getHeader(HttpHeaders.SET_COOKIE)).isEqualTo(("foo=bar; Path=/path; Domain=example.com; " + + "Max-Age=0; Expires=Thu, 01 Jan 1970 00:00:00 GMT; " + + "Secure; HttpOnly; Partitioned")); + } + + @Test + void setCookieHeader() { + response.setHeader(HttpHeaders.SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax"); + assertNumCookies(1); + assertPrimarySessionCookie("123"); + + // Setting the Set-Cookie header a 2nd time should overwrite the previous value + response.setHeader(HttpHeaders.SET_COOKIE, "SESSION=999; Path=/; Secure; HttpOnly; SameSite=Lax"); + assertNumCookies(1); + assertPrimarySessionCookie("999"); + } + + @Test + void setCookieHeaderWithMaxAgeAndExpiresAttributes() { + String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=" + expiryDate + "; Secure; HttpOnly; SameSite=Lax"; + response.setHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertThat(response.getHeader(HttpHeaders.SET_COOKIE)).isEqualTo(cookieValue); + + assertNumCookies(1); + assertThat(response.getCookies()[0]).isInstanceOf(MockCookie.class); + MockCookie mockCookie = (MockCookie) response.getCookies()[0]; + assertThat(mockCookie.getMaxAge()).isEqualTo(100); + assertThat(mockCookie.getExpires()).isEqualTo(ZonedDateTime.parse(expiryDate, DateTimeFormatter.RFC_1123_DATE_TIME)); + } + + @Test + void setCookieHeaderWithZeroExpiresAttribute() { + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0"; + response.setHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertNumCookies(1); + String header = response.getHeader(HttpHeaders.SET_COOKIE); + assertThat(header).isNotEqualTo(cookieValue); + // We don't assert the actual Expires value since it is based on the current time. + assertThat(header).startsWith("SESSION=123; Path=/; Max-Age=100; Expires="); + } + + @Test + void addCookieHeader() { + response.addHeader(HttpHeaders.SET_COOKIE, "SESSION=123; Path=/; Secure; HttpOnly; SameSite=Lax"); + assertNumCookies(1); + assertPrimarySessionCookie("123"); + + // Adding a 2nd cookie header should result in 2 cookies. + response.addHeader(HttpHeaders.SET_COOKIE, "SESSION=999; Path=/; Secure; HttpOnly; SameSite=Lax"); + assertNumCookies(2); + assertPrimarySessionCookie("123"); + assertCookieValues("123", "999"); + } + + @Test + void addCookieHeaderWithMaxAgeAndExpiresAttributes() { + String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=" + expiryDate + "; Secure; HttpOnly; SameSite=Lax"; + response.addHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertThat(response.getHeader(HttpHeaders.SET_COOKIE)).isEqualTo(cookieValue); + + assertNumCookies(1); + assertThat(response.getCookies()[0]).isInstanceOf(MockCookie.class); + MockCookie mockCookie = (MockCookie) response.getCookies()[0]; + assertThat(mockCookie.getMaxAge()).isEqualTo(100); + assertThat(mockCookie.getExpires()).isEqualTo(ZonedDateTime.parse(expiryDate, DateTimeFormatter.RFC_1123_DATE_TIME)); + } + + @Test + void addCookieHeaderWithMaxAgeAndZeroExpiresAttributes() { + String cookieValue = "SESSION=123; Path=/; Max-Age=100; Expires=0"; + response.addHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertNumCookies(1); + String header = response.getHeader(HttpHeaders.SET_COOKIE); + assertThat(header).isNotEqualTo(cookieValue); + // We don't assert the actual Expires value since it is based on the current time. + assertThat(header).startsWith("SESSION=123; Path=/; Max-Age=100; Expires="); + } + + @Test + void addCookieHeaderWithExpiresAttributeWithoutMaxAgeAttribute() { + String expiryDate = "Tue, 8 Oct 2019 19:50:00 GMT"; + String cookieValue = "SESSION=123; Path=/; Expires=" + expiryDate; + response.addHeader(HttpHeaders.SET_COOKIE, cookieValue); + assertThat(response.getHeader(HttpHeaders.SET_COOKIE)).isEqualTo(cookieValue); + + assertNumCookies(1); + assertThat(response.getCookies()[0]).isInstanceOf(MockCookie.class); + MockCookie mockCookie = (MockCookie) response.getCookies()[0]; assertThat(mockCookie.getName()).isEqualTo("SESSION"); - assertThat(mockCookie.getValue()).isEqualTo(expectedValue); + assertThat(mockCookie.getValue()).isEqualTo("123"); assertThat(mockCookie.getPath()).isEqualTo("/"); - assertThat(mockCookie.getSecure()).isTrue(); - assertThat(mockCookie.isHttpOnly()).isTrue(); - assertThat(mockCookie.getComment()).isNull(); - assertThat(mockCookie.getExpires()).isNull(); - assertThat(mockCookie.getSameSite()).isEqualTo("Lax"); - }); - } - - @Test // gh-25501 - void resetResetsCharset() { - assertThat(response.getContentType()).isNull(); - assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); - assertThat(response.isCharset()).isFalse(); - response.setCharacterEncoding("UTF-8"); - assertThat(response.isCharset()).isTrue(); - assertThat(response.getCharacterEncoding()).isEqualTo("UTF-8"); - response.setContentType("text/plain"); - assertThat(response.getContentType()).isEqualTo("text/plain;charset=UTF-8"); - String contentTypeHeader = response.getHeader(CONTENT_TYPE); - assertThat(contentTypeHeader).isEqualTo("text/plain;charset=UTF-8"); - - response.reset(); - - assertThat(response.getContentType()).isNull(); - assertThat(response.getCharacterEncoding()).isEqualTo("ISO-8859-1"); - assertThat(response.isCharset()).isFalse(); - // Do not invoke setCharacterEncoding() since that sets the charset flag to true. - // response.setCharacterEncoding("UTF-8"); - response.setContentType("text/plain"); - assertThat(response.isCharset()).isFalse(); // should still be false - assertThat(response.getContentType()).isEqualTo("text/plain"); - contentTypeHeader = response.getHeader(CONTENT_TYPE); - assertThat(contentTypeHeader).isEqualTo("text/plain"); - } - - @Test // gh-33019 - void contentAsStringEncodingWithJson() throws IOException { - String content = "{\"name\": \"Jürgen\"}"; - response.setContentType(MediaType.APPLICATION_JSON_VALUE); - response.getWriter().write(content); - assertThat(response.getContentAsString()).isEqualTo(content); - } + assertThat(mockCookie.getMaxAge()).isEqualTo(-1); + assertThat(mockCookie.getExpires()).isEqualTo(ZonedDateTime.parse(expiryDate, DateTimeFormatter.RFC_1123_DATE_TIME)); + } + + @Test + void addCookie() { + MockCookie mockCookie = new MockCookie("SESSION", "123"); + mockCookie.setPath("/"); + mockCookie.setDomain("example.com"); + mockCookie.setMaxAge(0); + mockCookie.setSecure(true); + mockCookie.setHttpOnly(true); + mockCookie.setSameSite("Lax"); + + response.addCookie(mockCookie); + + assertNumCookies(1); + assertThat(response.getHeader(HttpHeaders.SET_COOKIE)).isEqualTo(("SESSION=123; Path=/; Domain=example.com; Max-Age=0; " + + "Expires=Thu, 01 Jan 1970 00:00:00 GMT; Secure; HttpOnly; SameSite=Lax")); + + // Adding a 2nd Cookie should result in 2 Cookies. + response.addCookie(new MockCookie("SESSION", "999")); + assertNumCookies(2); + assertCookieValues("123", "999"); + } + + private void assertNumCookies(int expected) { + assertThat(response.getCookies()).hasSize(expected); + } + + private void assertCookieValues(String... expected) { + assertThat(response.getCookies()).extracting(Cookie::getValue).containsExactly(expected); + } + + @SuppressWarnings("removal") + private void assertPrimarySessionCookie(String expectedValue) { + Cookie cookie = response.getCookie("SESSION"); + assertThat(cookie).asInstanceOf(type(MockCookie.class)).satisfies(mockCookie -> { + assertThat(mockCookie.getName()).isEqualTo("SESSION"); + assertThat(mockCookie.getValue()).isEqualTo(expectedValue); + assertThat(mockCookie.getPath()).isEqualTo("/"); + assertThat(mockCookie.getSecure()).isTrue(); + assertThat(mockCookie.isHttpOnly()).isTrue(); + assertThat(mockCookie.getComment()).isNull(); + assertThat(mockCookie.getExpires()).isNull(); + assertThat(mockCookie.getSameSite()).isEqualTo("Lax"); + }); + } + + } + + @Nested + class ResponseCommittedTests { + + @Test + void servletOutputStreamCommittedWhenBufferSizeExceeded() throws IOException { + assertThat(response.isCommitted()).isFalse(); + response.getOutputStream().write('X'); + assertThat(response.isCommitted()).isFalse(); + int size = response.getBufferSize(); + response.getOutputStream().write(new byte[size]); + assertThat(response.isCommitted()).isTrue(); + assertThat(response.getContentAsByteArray()).hasSize((size + 1)); + } + + @Test + void servletOutputStreamCommittedOnFlushBuffer() throws IOException { + assertThat(response.isCommitted()).isFalse(); + response.getOutputStream().write('X'); + assertThat(response.isCommitted()).isFalse(); + response.flushBuffer(); + assertThat(response.isCommitted()).isTrue(); + assertThat(response.getContentAsByteArray()).hasSize(1); + } + + @Test + void servletWriterCommittedWhenBufferSizeExceeded() throws IOException { + assertThat(response.isCommitted()).isFalse(); + response.getWriter().write("X"); + assertThat(response.isCommitted()).isFalse(); + int size = response.getBufferSize(); + char[] data = new char[size]; + Arrays.fill(data, 'p'); + response.getWriter().write(data); + assertThat(response.isCommitted()).isTrue(); + assertThat(response.getContentAsByteArray()).hasSize((size + 1)); + } + + @Test + void servletOutputStreamCommittedOnOutputStreamFlush() throws IOException { + assertThat(response.isCommitted()).isFalse(); + response.getOutputStream().write('X'); + assertThat(response.isCommitted()).isFalse(); + response.getOutputStream().flush(); + assertThat(response.isCommitted()).isTrue(); + assertThat(response.getContentAsByteArray()).hasSize(1); + } + + @Test + void servletWriterCommittedOnWriterFlush() throws IOException { + assertThat(response.isCommitted()).isFalse(); + response.getWriter().write("X"); + assertThat(response.isCommitted()).isFalse(); + response.getWriter().flush(); + assertThat(response.isCommitted()).isTrue(); + assertThat(response.getContentAsByteArray()).hasSize(1); + } + + @Test // SPR-16683 + void servletWriterCommittedOnWriterClose() throws IOException { + assertThat(response.isCommitted()).isFalse(); + response.getWriter().write("X"); + assertThat(response.isCommitted()).isFalse(); + response.getWriter().close(); + assertThat(response.isCommitted()).isTrue(); + assertThat(response.getContentAsByteArray()).hasSize(1); + } + + } + + @Nested + class ResponseBodyTests { + + @Test // gh-26493 + void setLocaleWithNullValue() { + assertThat(response.getLocale()).isEqualTo(Locale.getDefault()); + response.setLocale(null); + assertThat(response.getLocale()).isEqualTo(Locale.getDefault()); + } + + @Test // gh-23219 + void contentAsUtf8() throws IOException { + String content = "Příliš žluťoučký kůň úpěl ďábelské ódy"; + response.getOutputStream().write(content.getBytes(StandardCharsets.UTF_8)); + assertThat(response.getContentAsString(StandardCharsets.UTF_8)).isEqualTo(content); + } + + @Test + void servletWriterAutoFlushedForChar() throws IOException { + response.getWriter().write('X'); + assertThat(response.getContentAsString()).isEqualTo("X"); + } + + @Test + void servletWriterAutoFlushedForCharArray() throws IOException { + response.getWriter().write("XY".toCharArray()); + assertThat(response.getContentAsString()).isEqualTo("XY"); + } + + @Test + void servletWriterAutoFlushedForString() throws IOException { + response.getWriter().write("X"); + assertThat(response.getContentAsString()).isEqualTo("X"); + } + + @Test + void sendRedirect() throws IOException { + String redirectUrl = "/redirect"; + response.sendRedirect(redirectUrl); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_MOVED_TEMPORARILY); + assertThat(response.getHeader(HttpHeaders.LOCATION)).isEqualTo(redirectUrl); + assertThat(response.getRedirectedUrl()).isEqualTo(redirectUrl); + assertThat(response.isCommitted()).isTrue(); + } + + @Test + void locationHeaderUpdatesGetRedirectedUrl() { + String redirectUrl = "/redirect"; + response.setHeader(HttpHeaders.LOCATION, redirectUrl); + assertThat(response.getRedirectedUrl()).isEqualTo(redirectUrl); + } + + @Test // SPR-10414 + void modifyStatusAfterSendError() throws IOException { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + response.setStatus(HttpServletResponse.SC_OK); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); + } + + @Test // SPR-10414 + void modifyStatusMessageAfterSendError() throws IOException { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + assertThat(response.getStatus()).isEqualTo(HttpServletResponse.SC_NOT_FOUND); + } + + @Test // gh-33019 + void contentAsStringEncodingWithJson() throws IOException { + String content = "{\"name\": \"Jürgen\"}"; + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.getWriter().write(content); + assertThat(response.getContentAsString()).isEqualTo(content); + } + + @Test + void writerShouldFailWhenOutputStreamCalled() { + response.getOutputStream(); + assertThatIllegalStateException().isThrownBy(() -> response.getWriter()); + } + + @Test + void outputStreamShouldFailWhenWriterCalled() throws IOException { + response.getWriter(); + assertThatIllegalStateException().isThrownBy(() -> response.getOutputStream()); + } - @Test // gh-34488 - void shouldAddMultipleContentLanguage() { - response.addHeader("Content-Language", "en"); - response.addHeader("Content-Language", "fr"); - assertThat(response.getHeaders("Content-Language")).contains("en", "fr"); - assertThat(response.getLocale()).isEqualTo(Locale.ENGLISH); } } diff --git a/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java b/spring-test/src/test/java/org/springframework/test/annotation/ProfileValueUtilsTests.java index cda7ef180522..d3d8a564c502 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 @@ -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 @@ * @author Sam Brannen * @since 3.0 */ +@SuppressWarnings("deprecation") class ProfileValueUtilsTests { private static final String NON_ANNOTATED_METHOD = "nonAnnotatedMethod"; @@ -47,35 +48,45 @@ static void setProfileValue() { } private void assertClassIsEnabled(Class testClass) { - assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testClass)).as("Test class [" + testClass + "] should be enabled.").isTrue(); + assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testClass)) + .as("Test class [" + testClass + "] should be enabled.") + .isTrue(); } private void assertClassIsDisabled(Class testClass) { - assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testClass)).as("Test class [" + testClass + "] should be disabled.").isFalse(); + assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testClass)) + .as("Test class [" + testClass + "] should be disabled.") + .isFalse(); } private void assertMethodIsEnabled(String methodName, Class testClass) throws Exception { Method testMethod = testClass.getMethod(methodName); - assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, testClass)).as("Test method [" + testMethod + "] should be enabled.").isTrue(); + assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, testClass)) + .as("Test method [" + testMethod + "] should be enabled.") + .isTrue(); } private void assertMethodIsDisabled(String methodName, Class testClass) throws Exception { Method testMethod = testClass.getMethod(methodName); - assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, testClass)).as("Test method [" + testMethod + "] should be disabled.").isFalse(); + assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(testMethod, testClass)) + .as("Test method [" + testMethod + "] should be disabled.") + .isFalse(); } private void assertMethodIsEnabled(ProfileValueSource profileValueSource, String methodName, Class testClass) throws Exception { Method testMethod = testClass.getMethod(methodName); - assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(profileValueSource, testMethod, testClass)).as("Test method [" + testMethod + "] should be enabled for ProfileValueSource [" + profileValueSource - + "].").isTrue(); + assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(profileValueSource, testMethod, testClass)) + .as("Test method [" + testMethod + "] should be enabled for ProfileValueSource [" + profileValueSource + "].") + .isTrue(); } private void assertMethodIsDisabled(ProfileValueSource profileValueSource, String methodName, Class testClass) throws Exception { Method testMethod = testClass.getMethod(methodName); - assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(profileValueSource, testMethod, testClass)).as("Test method [" + testMethod + "] should be disabled for ProfileValueSource [" + profileValueSource - + "].").isFalse(); + assertThat(ProfileValueUtils.isTestEnabledInThisEnvironment(profileValueSource, testMethod, testClass)) + .as("Test method [" + testMethod + "] should be disabled for ProfileValueSource [" + profileValueSource + "].") + .isFalse(); } // ------------------------------------------------------------------- @@ -130,7 +141,6 @@ void isTestEnabledInThisEnvironmentForProvidedMethodAndClass() throws Exception @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 41388fc7da2c..57d6ad93f3db 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.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.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 @@ void methodLevelDirtiesContextWithExhaustiveHierarchyMode() { 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 @@ private void runTestAndVerifyHierarchies(Class testClass, // ------------------------------------------------------------------------- - @RunWith(SpringRunner.class) + @ExtendWith(SpringExtension.class) @ContextHierarchy(@ContextConfiguration(name = "foo")) abstract static class FooTestCase implements ApplicationContextAware { @@ -170,10 +172,10 @@ String bean() { * 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 @@ public void test() { * 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 @@ public void test() { * 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 @@ public void test() { *

      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/OverriddenMetaAnnotationAttributesTestContextAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/OverriddenMetaAnnotationAttributesTestContextAnnotationUtilsTests.java index 116f615f2250..272317d4e961 100644 --- a/spring-test/src/test/java/org/springframework/test/context/OverriddenMetaAnnotationAttributesTestContextAnnotationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/OverriddenMetaAnnotationAttributesTestContextAnnotationUtilsTests.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,8 +34,6 @@ *

      See SPR-10181. * * @author Sam Brannen - * @since 5.3, though originally since 4.0 for the deprecated - * {@link org.springframework.test.util.MetaAnnotationUtils} support * @see TestContextAnnotationUtilsTests */ class OverriddenMetaAnnotationAttributesTestContextAnnotationUtilsTests { 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 4500d5566899..cb4b809abffe 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 @@ @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/TestContextAnnotationUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java index 1f105660a42a..83459999b59c 100644 --- a/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/TestContextAnnotationUtilsTests.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,6 @@ * Tests for {@link TestContextAnnotationUtils}. * * @author Sam Brannen - * @since 5.3, though originally since 4.0 for the deprecated - * {@link org.springframework.test.util.MetaAnnotationUtils} support * @see OverriddenMetaAnnotationAttributesTestContextAnnotationUtilsTests */ class TestContextAnnotationUtilsTests { diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderRuntimeHintsTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderRuntimeHintsTests.java index 2da0577c23f3..1ffc88cd39d0 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderRuntimeHintsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderRuntimeHintsTests.java @@ -49,7 +49,7 @@ void aotContextLoaderCanRegisterRuntimeHints() { generator.processAheadOfTime(Stream.of(TestCase.class)); - assertThat(reflection().onMethod(ConfigWithMain.class, "main").invoke()).accepts(runtimeHints); + assertThat(reflection().onMethodInvocation(ConfigWithMain.class, "main")).accepts(runtimeHints); } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderTests.java index e82ff54f1f86..4fa2de4a5b31 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotContextLoaderTests.java @@ -25,9 +25,9 @@ import org.springframework.test.context.MergedContextConfiguration; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.never; -import static org.mockito.BDDMockito.spy; import static org.mockito.BDDMockito.then; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; /** * Unit tests for {@link AotContextLoader}. 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 e5fa0317f832..aa9eae8800ce 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 @@ void endToEndTestsForEntireSpringTestModule() { .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 @@ void endToEndTestsForBeanOverrides() { 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/DeclarativeRuntimeHintsTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/DeclarativeRuntimeHintsTests.java index e983a5fa9743..57cf1206b7e9 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/DeclarativeRuntimeHintsTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/DeclarativeRuntimeHintsTests.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. @@ -56,7 +56,7 @@ void declarativeRuntimeHints() { // @RegisterReflectionForBinding assertReflectionRegistered(SampleClassWithGetter.class); assertReflectionRegistered(String.class); - assertThat(reflection().onMethod(SampleClassWithGetter.class, "getName")).accepts(this.runtimeHints); + assertThat(reflection().onMethodInvocation(SampleClassWithGetter.class, "getName")).accepts(this.runtimeHints); // @ImportRuntimeHints assertThat(resource().forResource("org/example/config/enigma.txt")).accepts(this.runtimeHints); diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java index daa669124fe2..4f971c777c38 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestAotProcessorTests.java @@ -79,8 +79,7 @@ void process(@TempDir(cleanup = CleanupMode.ON_SUCCESS) Path tempDir) throws Exc assertThat(findFiles(sourceOutput)).containsExactlyInAnyOrderElementsOf(expectedSourceFiles()); assertThat(findFiles(resourceOutput.resolve("META-INF/native-image"))).contains( - Path.of(groupId, artifactId, "reflect-config.json"), - Path.of(groupId, artifactId, "resource-config.json")); + Path.of(groupId, artifactId, "reachability-metadata.json")); } private void copy(Class testClass, Path destination) { diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java index e5b0bf2250c5..a71890a09cf6 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.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. @@ -290,7 +290,7 @@ private static void assertRuntimeHints(RuntimeHints runtimeHints) { ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS)); // @TestBean(methodName = ) - assertThat(reflection().onMethod(GreetingServiceFactory.class, "createEnigmaGreetingService")) + assertThat(reflection().onMethodInvocation(GreetingServiceFactory.class, "createEnigmaGreetingService")) .accepts(runtimeHints); // GenericApplicationContext.preDetermineBeanTypes() should have registered proxy 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 2e9dd795cf4e..de275583b88b 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 @@ void failOnErrorDisabledViaSpringProperty(String value) { assertThat(createGenerator().failOnError).isFalse(); } + @Test // gh-34841 + void contextIsClosedAfterAotProcessing() { + DemoTestContextAotGenerator generator = createGenerator(); + generator.processAheadOfTime(Stream.of(TestCase1.class, TestCase2.class)); + + 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 { - private static TestContextAotGenerator createGenerator() { - return new TestContextAotGenerator(null); + @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 4e1189674b04..e9186c0961fa 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 @@ // 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/bean/override/MockitoBeanJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java index f914c84c5d6b..709c9e82c0dd 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.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,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.when; /** * @author Sam Brannen 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 d18c2068388e..14c3e1b9df7e 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 @@ @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 1e67f5dc320a..0fe9cffbf6f1 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 @@ @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 2ed2498993e2..2e1691467365 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.lang.Nullable; +import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor.DummyBeanOverrideHandler; import static org.assertj.core.api.Assertions.assertThat; @@ -90,9 +91,8 @@ private Consumer dummyHandler(@Nullable String beanName, Cl }; } - @Nullable - private BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { - return this.factory.createContextCustomizer(testClass, Collections.emptyList()); + private @Nullable BeanOverrideContextCustomizer createContextCustomizer(Class testClass) { + 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 9e01f72ca87e..3948db4ff87d 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,10 +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.lang.Nullable; +import org.springframework.test.context.ContextConfigurationAttributes; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; @@ -42,9 +44,8 @@ public abstract class BeanOverrideContextCustomizerTestUtils { * @param testClass a test class to introspect * @return a context customizer for bean override support, or null */ - @Nullable - public static ContextCustomizer createContextCustomizer(Class testClass) { - return factory.createContextCustomizer(testClass, Collections.emptyList()); + public static @Nullable ContextCustomizer createContextCustomizer(Class testClass) { + 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 8944aeb2be3f..17a32878fc00 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. @@ -20,11 +20,11 @@ import java.util.LinkedHashSet; import java.util.Objects; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.util.ReflectionUtils; import static org.assertj.core.api.Assertions.assertThat; @@ -72,7 +72,7 @@ private static class DummyBeanOverrideHandler extends BeanOverrideHandler { 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 5229cf5b44dd..49c8d8500751 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 @@ -26,10 +26,11 @@ import java.util.List; import java.util.function.Consumer; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Qualifier; -import org.springframework.lang.Nullable; +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 @@ void isEqualToWithSameMetadata() { } @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 @@ void isEqualToWithSameMetadataAndBeanNames() { } @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 @@ void isNotEqualToWithSameMetadataAndDifferentQualifierValues() { 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 @@ void isNotEqualToWithSameMetadataAndDifferentQualifiers() { 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 @@ void isNotEqualToWithByTypeLookupAndDifferentFieldNames() { 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 @@ private static BeanOverrideHandler createBeanOverrideHandler(Field field) { } 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 @@ static class MultipleAnnotations { 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 000000000000..cb0018d3a208 --- /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 d6beaf4ba306..4d7a14766706 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. @@ -24,15 +24,16 @@ import java.lang.annotation.Target; import java.lang.reflect.Field; +import org.jspecify.annotations.Nullable; + import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.DummyBean.DummyBeanOverrideProcessor; 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 @@ -45,6 +46,8 @@ String beanName() default ""; + String contextName() default ""; + BeanOverrideStrategy strategy() default BeanOverrideStrategy.REPLACE; class DummyBeanOverrideProcessor implements BeanOverrideProcessor { @@ -54,7 +57,7 @@ public BeanOverrideHandler createHandler(Annotation annotation, Class testCla 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 @@ -62,9 +65,9 @@ public BeanOverrideHandler createHandler(Annotation annotation, Class testCla 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 f95fe62912a7..b47ea30c1305 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 @@ private static TestBeanOverrideHandler handlerFor(Field field, Method overrideMe 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/TestBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java index c5cacb9472d9..4d04b9543594 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/convention/TestBeanOverrideProcessorTests.java @@ -19,10 +19,10 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; +import org.jspecify.annotations.NonNull; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.NonNull; import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.test.context.bean.override.example.TestBeanFactory; import org.springframework.util.ReflectionUtils; 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 000000000000..a59c59bfa0e0 --- /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 000000000000..8df069273a40 --- /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 000000000000..e2f3ec516c60 --- /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 000000000000..b1e8461fe032 --- /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 000000000000..b7e021528eb9 --- /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 000000000000..5fb01297c768 --- /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 d93fafb7837d..c121fa30c6e6 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. @@ -20,11 +20,11 @@ import org.easymock.EasyMock; import org.easymock.MockType; +import org.jspecify.annotations.Nullable; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.SingletonBeanRegistry; import org.springframework.core.ResolvableType; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import static org.springframework.test.context.bean.override.BeanOverrideStrategy.REPLACE_OR_CREATE; @@ -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/example/package-info.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/package-info.java index c642f01155f6..e140d79198de 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/example/package-info.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/example/package-info.java @@ -1,9 +1,7 @@ /** * Example components for testing spring-test Bean overriding feature. */ -@NonNullApi -@NonNullFields +@NullMarked package org.springframework.test.context.bean.override.example; -import org.springframework.lang.NonNullApi; -import org.springframework.lang.NonNullFields; +import org.jspecify.annotations.NullMarked; 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 000000000000..84fd8be1433b --- /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/MockitoBeanForBrokenFactoryBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForBrokenFactoryBeanIntegrationTests.java index 4f8d76b0557b..fcd8535c39e8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForBrokenFactoryBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForBrokenFactoryBeanIntegrationTests.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,7 +26,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.when; /** * Test {@link MockitoBean @MockitoBean} for a {@link FactoryBean} that is diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java index 72f2d03435be..36414c8d878e 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForByTypeLookupIntegrationTests.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,10 +33,10 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.BDDMockito.when; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; import static org.springframework.test.mockito.MockitoAssertions.assertIsMock; /** diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForFactoryBeanIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForFactoryBeanIntegrationTests.java index c8722a8a4a32..41fe6a15e4fe 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForFactoryBeanIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanForFactoryBeanIntegrationTests.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.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.when; /** * Test {@link MockitoBean @MockitoBean} for a factory bean configuration. diff --git a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanManuallyRegisteredSingletonTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanManuallyRegisteredSingletonTests.java index 4cb2d8cfecdc..621312abf1f4 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanManuallyRegisteredSingletonTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanManuallyRegisteredSingletonTests.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,7 +24,7 @@ import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.when; /** * Verifies support for overriding a manually registered singleton bean with 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 72babe686745..7934e07f7bdc 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 @@ /** * 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 @@ abstract class AbstractBaseClassForNestedTests { 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 000000000000..bc43837b2441 --- /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 466bcd93e348..1875e2e2d631 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 @@ void isEqualToWithSameByNameLookupMetadataFromFieldAndClassLevel() { /** * 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/MockitoBeanOverrideProcessorTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java index 5ac9041ac6c5..17bb286c1725 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanOverrideProcessorTests.java @@ -20,11 +20,11 @@ import java.lang.reflect.Field; import java.util.List; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.core.annotation.AnnotationUtils; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.BeanOverrideHandler; import org.springframework.util.ReflectionUtils; @@ -89,10 +89,9 @@ void typesNotSupportedAtFieldLevel() { static class TestCase { - @Nullable @MockitoBean @MockitoSpyBean - Integer number; + public @Nullable Integer number; @MockitoBean(types = Integer.class) String typesNotSupported; 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 000000000000..95d4ac845858 --- /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/MockitoBeanWithResetIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithResetIntegrationTests.java index d75e512170e4..45846a83b2c1 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithResetIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/MockitoBeanWithResetIntegrationTests.java @@ -16,6 +16,7 @@ package org.springframework.test.context.bean.override.mockito; +import org.jspecify.annotations.Nullable; import org.junit.jupiter.api.MethodOrderer.OrderAnnotation; import org.junit.jupiter.api.Order; import org.junit.jupiter.api.Test; @@ -25,7 +26,6 @@ import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.lang.Nullable; import org.springframework.test.context.bean.override.example.ExampleService; import org.springframework.test.context.bean.override.example.FailingExampleService; import org.springframework.test.context.bean.override.example.RealExampleService; @@ -88,15 +88,13 @@ void factoryBeanSecondEnsuringMockReset(ApplicationContext ctx) { } static class FailingExampleServiceFactory implements FactoryBean { - @Nullable @Override - public FailingExampleService getObject() { + public @Nullable FailingExampleService getObject() { return new FailingExampleService(); } - @Nullable @Override - public Class getObjectType() { + public @Nullable Class getObjectType() { return FailingExampleService.class; } } diff --git a/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java similarity index 75% rename from spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.java index b563b4d37973..1b71868b4bbe 100644 --- a/spring-context/src/test/java/example/indexed/IndexedJavaxManagedBeanComponent.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/BarService.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. @@ -14,11 +14,11 @@ * limitations under the License. */ -package example.indexed; +package org.springframework.test.context.bean.override.mockito.hierarchies; -/** - * @author Sam Brannen - */ -@javax.annotation.ManagedBean -public class IndexedJavaxManagedBeanComponent { +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 000000000000..5877553e1acf --- /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-context/src/test/java/example/scannable/JavaxNamedComponent.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java similarity index 74% rename from spring-context/src/test/java/example/scannable/JavaxNamedComponent.java rename to spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.java index a0fe78e7429a..ab2ee99fc965 100644 --- a/spring-context/src/test/java/example/scannable/JavaxNamedComponent.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/hierarchies/FooService.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. @@ -14,11 +14,11 @@ * limitations under the License. */ -package example.scannable; +package org.springframework.test.context.bean.override.mockito.hierarchies; -/** - * @author Sam Brannen - */ -@javax.inject.Named("myJavaxNamedComponent") -public class JavaxNamedComponent { +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 00950dcd03fd..98d633a12b70 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 @@ void configureServiceMock() { } @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 000000000000..e452b50830f6 --- /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 000000000000..539611a27f4b --- /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 000000000000..01832db8fd23 --- /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 000000000000..d6421b208146 --- /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 000000000000..f212d309a803 --- /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 000000000000..6a1f281cf2da --- /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 b5f02fa893f0..aef8cd39cbd4 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 000000000000..63c3561d0881 --- /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 000000000000..255f3630beac --- /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 000000000000..951d37b96215 --- /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 000000000000..13e1eba1b12f --- /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 000000000000..1f71a5e674f1 --- /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 000000000000..90cf7d285699 --- /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 000000000000..3d0d841c8f1e --- /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 000000000000..e4fb4d16c3e0 --- /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 000000000000..b5dc403a727b --- /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 000000000000..009d85e16353 --- /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/MockitoBeanAndGenericsIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndGenericsIntegrationTests.java index 50f7d09e6634..9879755f29ad 100644 --- a/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndGenericsIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/bean/override/mockito/integration/MockitoBeanAndGenericsIntegrationTests.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,7 @@ import org.springframework.test.context.bean.override.mockito.integration.AbstractMockitoBeanAndGenericsIntegrationTests.ThingImpl; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.BDDMockito.when; +import static org.mockito.Mockito.when; /** * Concrete implementation of {@link AbstractMockitoBeanAndGenericsIntegrationTests}. 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 000000000000..63e9b6ee07de --- /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 000000000000..acf1fd66f499 --- /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 000000000000..f3d1fc1c378b --- /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 000000000000..197eedc5b0a4 --- /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 f0663a50e2c0..033eb1b0270d 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.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 @@ static void verifyFinalCacheState() { // ------------------------------------------------------------------- - @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 a5527be66e0b..1ed24d6a84f1 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 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.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 @@ void verifyDirtiesContextBehavior() throws Exception { 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 @@ static void verifyFinalCacheState() { // ------------------------------------------------------------------- - @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 @@ static void verifyFinalCacheState() { }) abstract static class BaseTestCase { - @Configuration + @Configuration(proxyBeanMethods = false) static class Config { /* no beans */ } @@ -175,75 +184,75 @@ protected void assertApplicationContextWasAutowired() { } } - 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 72dbddc0eaf5..323d7eb6d329 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.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 1e67f031453b..2c0861c8a26a 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.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 @@ * @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 56caa8c9034e..4cc1559eaff0 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 cc368b2862b2..e55e6840efd6 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 @@ * @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 8436a50c95d8..d46cda1c463d 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 9e0b2378dd97..f9b1b3db0c03 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 @@ * @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 Employee employee() { } } - - @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 31fb3d0ecfcf..c2170704e9e5 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 @@ * @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 53% 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 869664fe0b9e..df4c3f7aae5f 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,36 @@ * 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; + public static final String CLASSPATH_CONTEXT_RESOURCE_PATH = ResourceUtils.CLASSPATH_URL_PREFIX + + 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 000000000000..485e128b6bc1 --- /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 4bbc906dd009..4d00fba6a647 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.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: * *